//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //+------------------------------------------------------------------+ #include "ExpertSignalCustom.mqh" #include "..\AI\Network.mqh" #include "..\Variables\IndicatorResources.mqh" #include "..\Variables\IndicatorTuneRanges.mqh" #include "..\System\StatusLabel.mqh" #include "..\System\SharedFileCopy.mqh" #include "..\System\NewsRelevance.mqh" #include "..\System\CrossAsset.mqh" #include "..\System\AltData.mqh" #include "ADIndicatorTuner.mqh" #include "..\System\BinomialStats.mqh" //--- The read-only view training-side collaborators depend on, and the adapter that lets this //--- class satisfy it without inheriting it (MQL5 gives a class exactly one base). #include "Training\ITrainingData.mqh" #include "Training\AIBaseTrainingData.mqh" //--- Same shape, chart-side: the read-only view CChartUI depends on for arrows, the status panel //--- and the HUD line. CChartUI itself (Chart\ChartUI.mqh) is included further down, next to //--- Training\BaselineComparator.mqh - it needs MAX_PERSISTED_ARROWS/ARROW_RESTORE_BUDGET_MS etc., //--- which are #defined later in this file, before it can be parsed. #include "Chart\IChartView.mqh" #include "Chart\AIBaseChartView.mqh" //--- Same shape again, persistence-side: the read+write view CModelPersistence depends on for the //--- .cfg/.stats sidecars, CPU-inference validation and net-load retry. CModelPersistence itself //--- (Persistence\ModelPersistence.mqh) is included further down, next to Chart\ChartUI.mqh - it //--- needs CPU_INFERENCE_MAX_DIFF, #defined later in this file. #include "Persistence\IPersistenceView.mqh" #include "Persistence\AIBasePersistenceView.mqh" #include //--- Alglib forest / least-squares for Training\BaselineComparator.mqh. Also pulls in statistics.mqh, where the //--- signal-database ranking's significance tests come from - one include serves both. #include //--- FFT cross-correlation for the all-lags profile. Not reached by dataanalysis.mqh. #include //--- Soft one-hot targets for the 3-neuron head. The per-neuron SIGMOID forward pass can saturate before //--- the softmax normalises, and a literal 1.0/0.0 target it only approaches asymptotically grows //--- weights toward the MAX_WEIGHT clamp. The excursion head trains on hard 1/0 instead. #define LABEL_SMOOTH_HIGH 0.9 #define LABEL_SMOOTH_LOW 0.05 //--- Control-panel object namespace. CAppDialog names every control from the dialog name, so one prefix //--- covers the tree. Declared here so it can appear in the chart-prefix sweep list below. //--- (SIG_ARROW_PREFIX moved to ExpertSignalCustom.mqh when the classic signals started drawing too.) #define WARRIOR_PANEL_PREFIX "WarriorCP" //--- Base of the PER-INSTANCE custom event id space for the training "study" event. Per-instance because //--- a shared id made every member run a train chunk for every other member's event - N*N chunks, and a //--- completely dead control panel - and because id 1 is the Controls library's own ON_DBL_CLICK. #define STUDY_EVENT_ID_BASE 500 //--- An armed study event this old that never arrived is declared lost and re-armed. Generous: a queued //--- event can legitimately wait tens of seconds behind a sibling's warm-up diagnostics. #define STUDY_EVENT_LOST_MS 60000 //--- Next unassigned study-event id, claimed in the constructor - numbers this chart's members 0..N-1. int g_warriorStudyEventSeq = 0; //+------------------------------------------------------------------+ //| ENSEMBLE CHART-LEVEL SHARED STATE (2+ direction NNs enabled). | //| Era barrier, combined-vote OOS score and warm-up sharing are | //| chart-level because they are questions about what gets TRADED. | //| Solo charts register nothing and none of it runs. | //+------------------------------------------------------------------+ class CExpertSignalAIBase; CExpertSignalAIBase *g_warriorEnsemble[]; //--- Combined-vote OOS rows for the current era; a stale-era contribution resets the buffer. //--- TWO masks: Mask = who EVALUATED this bar, VoterMask = who cast a NON-ZERO vote. VoterMask is the //--- divisor, because Direction() skips abstentions in both the sum and the count. datetime g_ensVoteTime[]; double g_ensVoteSum[]; //--- The same contributions UNSUMMED, one slot per member per bar - the decomposition g_ensVoteSum //--- destroys. Read by the combining-weight fit in Training\BaselineComparator.mqh. double g_ensVoteMember[]; int g_ensVoteMask[]; int g_ensVoteVoterMask[]; //--- The weighted mean's DIVISOR, carried per row rather than recomputed at verdict time: a member's //--- m_weight can be rewritten by UpdateSignalsWeights() between the scan and the verdict, and the //--- divisor must be the one in force when the numerator was accumulated. double g_ensVoteWeightSum[]; bool g_ensVoteLabelBuy[]; // the bar's swing label, per side - the vote's truth and its chance rate bool g_ensVoteLabelSell[]; bool g_ensVoteDirLabel[]; // bar carried a Buy/Sell label - the coverage floor's base rate //--- WHAT THE CALL WAS WORTH, in ATR units at the signal bar. The deploy gate certifies PRECISION //--- against a chance rate and has never known whether a correct call pays for its own spread - and //--- this project has watched several edges die on exactly that gap, at the point where precision was //--- already believed. Measured over a FIXED horizon of round(SwingLifespanEstimate()) bars, the same //--- label lifespan the effective-sample-size deflation uses, so the precision number and the payoff //--- number describe the SAME window and can be read in one sentence. //--- //--- POLICY-FREE BY CONSTRUCTION: no stop, no target, no trailing rule. This measures the SIGNAL, not //--- a trade-management choice layered on top of it - exit shaping moves payoff around without //--- creating any (see the exit-management verdict), so mixing the two here would only hide which of //--- them was responsible. Stored unsigned by direction (up and down excursions kept apart); the sign //--- is applied at verdict time from the vote's own direction, so one row serves a long read and a //--- short read identically and no row has to be measured twice. //--- TWO HORIZONS, BECAUSE ONE OF THEM CANNOT ANSWER THE QUESTION. The label fires when a pivot //--- lands WITHIN PIVOT_LABEL_TOLERANCE_BARS bars - so at that horizon the pivot may only just have //--- happened, and a perfectly correct call can still show a negative forward move because the turn //--- it predicted has not had a single bar to run yet. Measuring only there would understate, and //--- could invert, the payoff of a signal that is working exactly as designed. //--- SHORT = PIVOT_LABEL_TOLERANCE_BARS: "has the pivot arrived". A control, not the answer. //--- HOLD = that plus the median ZigZag leg: the pivot, PLUS the leg it opens. What a trade on //--- this call would actually be held for, and the horizon the payoff belongs to. //--- Reporting both is also the guard against picking one and calling it the truth - this project //--- has already had a break-even conclusion overturned purely by getting a horizon wrong. double g_ensVoteFwdR[]; // (close[bar-K] - close[bar]) / ATR[bar], signed by PRICE not by vote double g_ensVoteUpR[]; // (max high over the K forward bars - close[bar]) / ATR[bar], >= 0 double g_ensVoteDnR[]; // (close[bar] - min low over the K forward bars) / ATR[bar], >= 0 double g_ensVoteFwdR2[]; // the same three at the HOLD horizon double g_ensVoteUpR2[]; double g_ensVoteDnR2[]; bool g_ensVoteHasR2[]; // its own flag: the longer window runs off the leading edge sooner //--- Bars from the row's bar to the pivot its LABEL calls, -1 where it calls none. Used only to //--- BUCKET the payoff diagnostic - never to choose a per-call horizon, which would be a trap: d //--- exists only on bars the label got a pivot for, so a horizon that varied with d would give //--- correct and incorrect calls different windows and bias the comparison outright. int g_ensVoteD[]; //--- FALSE at the NEWEST K bars of the OOS slice, which have no forward window yet, and whenever the //--- ATR or a bar in the window is unusable. Those rows are DROPPED from the payoff tally rather than //--- counted as a zero move - the same leading-edge trap that made the lag profile's first run a //--- spectacular false positive (4113afd/bbe26a0). bool g_ensVoteHasR[]; int g_ensVoteRows = 0; long g_ensVoteEra = -1; int g_ensVoteDoneMask = 0; int g_ensVoteCursor[8]; // per-member monotonic row cursor (members scan bars in the same order) //--- Deliberately LARGER than MAX_AI_SIGNALS (5): independent caps, and over-allocating is free, whereas //--- matching would make this array silently too small the day the registry grows. #define ENS_MAX_MEMBERS 8 //+------------------------------------------------------------------+ //| Population count over the member masks. Bounded by the 8-slot | //| ensemble, so a plain loop is both clearest and fastest. | //+------------------------------------------------------------------+ int EnsembleBitCount(const int mask) { int n = 0; for(int b = 0; b < 8; b++) if((mask & (1 << b)) != 0) n++; return n; } //+------------------------------------------------------------------+ //| ENSEMBLE DEPLOY GATE. In ensemble mode THE UNIT OF EVALUATION IS | //| THE VOTE: best era, checkpointing, give-up and deploy all move | //| here, because all four ask what gets TRADED. | //+------------------------------------------------------------------+ double g_ensBestScore = -1.0; // best combined-vote selection score (precision x coverage credit) bool g_ensBestTradeable = false; // did that era clear the vote's own deployability floor bool g_ensBestTwoSided = false; // did it fire both long and short double g_ensBestPrecPct = -1.0; // the winning era's vote win rate, for the family-wise test double g_ensBestChancePct = -1.0; // and its chance reference int g_ensBestCalls = 0; // and the n that sets the standard error //--- WHICH of the deployability conditions the best era actually failed. The stage-3 refusal used to //--- say "no era's combined vote ever cleared the deployability floor" and then list all THREE //--- conditions in one parenthesis without saying which one fired - so an operator reading it could //--- not tell a coverage problem from a precision problem from a one-sided book, and the three have //--- nothing in common as fixes. Cost real time to diagnose by hand 2026-08-26, when the answer was //--- coverage every time. Same doctrine as CTrainPoolReader::Announce's reject list: a refusal that //--- will not say WHY is the failure this project has already paid for under several other names. //--- THE VOTE THRESHOLD'S CANDIDATE RUNGS. Rungs of PERCENTAGE_PRESETS, so every value here is one //--- an operator could also have selected by hand. NO LONGER DIAGNOSTIC (2026-08-26): the era verdict //--- now DERIVES the threshold from these instead of reading Signal_ThresholdOpen - see //--- the THE DERIVED THRESHOLD block in EnsembleEraVerdict() for the rule and the measurement. #define ENS_THRESHOLD_SWEEP_N 6 const double g_ensThresholdSweep[ENS_THRESHOLD_SWEEP_N] = {5.0, 10.0, 15.0, 20.0, 25.0, 30.0}; //--- The rung the era verdict derived, -1 before the first scored era. Published to the LIVE signal's //--- m_threshold_open by Warrior_EA.mq5 so the bar the gate certifies is the bar the EA trades - the //--- certified!=traded defect this project has already paid for once (2c443ba). double g_ensDerivedThreshold = -1.0; //--- SET BY ANY MEMBER THAT REBUILT FROM SCRATCH THIS RUN. The COMBINED-VOTE arrow store is //--- chart-scoped and keyed on the DB config fingerprint, which does not move when a model is //--- wiped - so it happily restored arrows drawn by models that no longer exist (observed //--- 2026-08-26: six charts restored 115-431 vote arrows onto a fleet training from era 0). //--- CVoteArrowStore::Discard() existed for exactly this and had NO CALLER. A vote is a claim made //--- by a specific set of members; if any one of them is fresh, the whole history is void. bool g_warriorFreshTopologyThisRun = false; double g_ensBestCoveragePct = -1.0; // what the best era's vote actually fired on double g_ensBestMinCoverPct = -1.0; // the floor it had to clear double g_ensBestEdgeFloorPct = -1.0; // and the precision bar, so all three are reportable long g_ensBestEra = -1; int g_ensCandidateEras = 0; // N for the family-wise correction: eras that COULD have won int g_ensErasSinceBest = 0; // shared plateau counter int g_ensPlateauStage = 0; // shared plateau stage //--- Which best-era the family-wise deploy test has already run against, -1 = none. Without it the //--- all-members-plateaued shortcut re-ran the gate against an unchanged best every era, incrementing //--- the candidate count the correction divides by - the run spent its time RAISING ITS OWN BAR. long g_ensGateTestedEra = -1; //--- One-shot latch so the collective IS-error plateau announces once per run, not once per era. bool g_ensIsPlateauAnnounced = false; bool g_ensDeployApproved = false; // stage 3 reached AND the vote cleared the family-wise gate long g_ensLastVerdictEra = -1; // guards against scoring one era twice //--- Lifetime combined-vote win rate over every bar the VOTE fired on, in the SAME shape as a solo //--- model's m_cumOosCorrect/m_cumOosTotal so the panel reads identically. Session-scoped like the rest //--- of the g_ens* ladder state. long g_ensCumOosCorrect = 0; long g_ensCumOosTotal = 0; //--- Mirror of Signal_ThresholdOpen, pushed in at registration so the combined-vote scorer fires on the //--- same criterion the live trade does. UNITS are the 0..100 VOTE scale, not a confidence percentage - //--- see LiveVoteContribution(). The seed is only read before registration overwrites it. double g_ensembleVoteThreshold = 60.0; //+------------------------------------------------------------------+ //| THE ENSEMBLE'S HEADLINE NUMBER, in one place. | //| | //| It used to be built inline at pass-3 completion and nowhere else, | //| which is why a restarted deployed chart had no aggregate line at | //| all: that code runs once per era, and a deployed ensemble runs no | //| further eras. Now it is a function, called from two places - the | //| era end (with this era's figure) and init (without one, from the | //| record restored out of .stats). | //| | //| WHAT THE NUMBER IS, and why it is the only one that belongs on a | //| deployed panel: the win rate of the COMBINED VOTE over the bars | //| the vote actually fired on - i.e. bars whose |vote| cleared | //| Signal_ThresholdOpen and whose side the direction policy allows. | //| A member's own precision counts every bar that member called Buy | //| or Sell, threshold or no threshold, which is not a quantity | //| anyone can trade. The threshold is NAMED in the text for the same | //| reason it is stored in the file: the number is meaningless | //| without it. | //+------------------------------------------------------------------+ void PublishEnsembleAccuracyLine(const double thisEraPrecPct, const int thisEraFired) { if(g_ensCumOosTotal <= 0) { g_ensembleVoteLine = (g_ensCandidateEras > 0) ? "Accuracy: no calls yet" : "Accuracy: measuring..."; return; } int winPct = (int)MathRound(g_ensCumOosCorrect * 100.0 / g_ensCumOosTotal); //--- THIS ERA alongside the lifetime figure - same reason as the solo panel's //--- ComputeCompoundedAccuracyLine: the lifetime average is diluted by every fired bar from every //--- prior era, so a real swing this era barely moves it. Omitted entirely at init, where there is //--- no "this era" and a stale one would read as live. //--- JUST THE NUMBER (2026-08-26). The call count, the threshold and the this-era figure all rode //--- along here; they are diagnostics, every one of them is in the era log line, and on a panel //--- they buried the one number anyone actually reads. The threshold in particular no longer needs //--- naming: it is derived and pinned rather than an operator's choice, so it is not a caveat on //--- the percentage any more. g_ensembleVoteLine = StringFormat("Accuracy: %d%%", winPct); } //--- Set by AdvancePatternDatabaseBackfill() on completion; lets OnTimer bypass the hourly DB-ranking //--- throttle ONCE so weights refresh from the fresh rows immediately. bool g_forcePatternWeightsRefresh = false; //--- Set by RankTiersFromOos() at every pass-3 completion; (re)arms the filtered-view overlay sweep. //--- A flag rather than an era comparison, because what the sweep needs is "a snapshot just got //--- fresher" - approximating that with era counters is how it used to re-arm against half-built caches. bool g_warriorOverlayArmRequest = false; //--- OVERLAY READINESS, one bit per ensemble member (the bit index IS m_ensembleIndex, which is that //--- member's slot in g_warriorEnsemble). Set at that member's pass-3 completion, cleared when a //--- sweep arms. //--- //--- WHY A MASK AND NOT A RATE LIMIT (2026-08-23): a member with no era-end snapshot returns false //--- from SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it BEFORE the divisor - so //--- one finished model's tier weight becomes the WHOLE vote and gets drawn as a consensus arrow. //--- The old code armed on the first member to finish and leaned on a 60 s limit to "collapse the //--- burst", on the assumption that members finish seconds apart. They do not: on USDJPY one member //--- was at sample 10496 while another was at 2304 of the same pass, minutes apart, so the sweep ran //--- with one voter and three silent members and put arrows on the chart for a consensus that did //--- not exist. AN ABSTENTION IS A MEMBER THAT LOOKED 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. uint g_warriorOverlayReadyMask = 0; //--- First tick a sweep was wanted but held for a missing member; 0 = not waiting. Bounds the hold, //--- because a member that stops (converged, stopped, error) would otherwise freeze the chart //--- forever - the be39674 lesson: a barrier must never silence the thing that reports it. uint g_warriorOverlayArmSince = 0; //+------------------------------------------------------------------+ //| EVERY chart-object namespace this EA creates, in ONE list - the | //| scattered call sites drifted and left stragglers behind. Add a | //| prefix here the moment a new object family appears. | //| Delete BY PREFIX, never ObjectsDeleteAll: a blanket wipe also | //| removes the user's own drawings. | //+------------------------------------------------------------------+ int WarriorChartPrefixes(string &out[]) { ArrayResize(out, 7); out[0] = SIG_ARROW_PREFIX; // directional signal arrows - bare prefix, so it also // matches every per-member namespace (WarSig_PAI_ ...) out[1] = STATUS_LABEL_PREFIX; // status line background + text (System\StatusLabel.mqh) out[2] = WARRIOR_PANEL_PREFIX; // control panel and its whole control tree out[3] = "WarriorAltMap_"; // alt-data symbol-mapping dialog (ADM_PREFIX in // Panel\AltDataMapDialog.mqh - literal here because that // header is included later in the build order) //--- CATCH-ALL. "Nothing matching our prefixes" and "the chart is clean" are different statements, //--- and only the first was checked - charts came up with duplicated panels after a purge reported //--- zero leftovers. Does NOT defeat skipArrows: "WarSig_" does not start with "Warrior". out[4] = "Warrior"; //--- Vote arrows, listed SEPARATELY from SIG_ARROW_PREFIX even though the name matches it: //--- skipArrows protects the per-model arrows because their sidecar is rebuilt by SCANNING them //--- off the chart. out[5] = SIG_VOTE_PREFIX; //--- The vote readout. Already covered by the catch-all, and listed anyway: the catch-all exists //--- because this list has drifted twice, not to make entries optional. out[6] = VOTE_HUD_PREFIX; return 7; } //+------------------------------------------------------------------+ //| Delete every object in those namespaces from a chart, and verify. | //| skipArrows spares the arrows for the one caller that must: a | //| re-init restores them from their sidecar, so wiping them flickers.| //| The rescan is required - object commands are QUEUED, so a bulk | //| call's return value is not evidence they are gone. Names are | //| collected before deleting: deleting while enumerating renumbers | //| the list being walked. | //+------------------------------------------------------------------+ int WarriorPurgeChartObjects(long chartID, bool skipArrows, int &leftoverCount) { string prefixes[]; int n = WarriorChartPrefixes(prefixes); int removed = 0; leftoverCount = 0; for(int p = 0; p < n; p++) { if(skipArrows && prefixes[p] == SIG_ARROW_PREFIX) continue; int r = ObjectsDeleteAll(chartID, prefixes[p]); if(r > 0) removed += r; } //--- Typed-blind rescan across EVERY object type: filtering on OBJ_ARROW made this blind in the same //--- way the bulk delete was, which is how two scans of one chart disagreed for three sessions. int total = ObjectsTotal(chartID, -1, -1); string leftovers[]; int found = 0; if(total > 0) { ArrayResize(leftovers, total); for(int i = 0; i < total; i++) { string nm = ObjectName(chartID, i, -1, -1); for(int p = 0; p < n; p++) { if(skipArrows && prefixes[p] == SIG_ARROW_PREFIX) continue; if(StringFind(nm, prefixes[p]) == 0) { leftovers[found++] = nm; break; } } } } for(int i = 0; i < found; i++) ObjectDelete(chartID, leftovers[i]); leftoverCount = found; return removed + found; } //--- Guard against a corrupt .arrows header declaring a garbage count. Restoring is chunked across timer //--- calls regardless, so a large-but-valid count costs progressive fill-in, never a frozen OnInit. #define MAX_RESTORED_ARROWS 50000 //--- How many of the MOST RECENT arrows stay on the chart and in the sidecar. Both save and load select //--- by TIME, not scan order - ObjectsTotal() order is arbitrary, so "the last N scanned" would keep a //--- random subset rather than the newest. #define MAX_PERSISTED_ARROWS 1000 //--- Prior strength RankTiersFromOos() shrinks each tier toward the pooled holdout win rate, //--- counted in EFFECTIVE observations. A tier carries ~8-15 of those per era, so at 10 it sits //--- about half on its own evidence. #define TIER_PRIOR_EFF_N 10.0 //--- Prior strength for the MODULE weight - how loudly this member speaks in the ensemble mean. //--- Deliberately far heavier than the tier prior because it is shrunk toward CHANCE, not toward //--- the member's own pooled rate: a member with almost no held-out fires must not be trusted at //--- whatever those few fires happened to show. Measured 2026-08-22, USDJPY: ConvLSTM fired 19 //--- times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the whole ensemble's //--- capable weight, off two effective observations, and the loudest voice on the chart. At 30 it //--- pulls that to ~0.15 while leaving a member with 300 effective calls essentially untouched. #define MODULE_PRIOR_EFF_N 30.0 //--- Wall-clock budget per chunk of the deferred arrow restore. MQL5 gives a chart ONE thread, so "async" //--- means small time-boxed slices, never one long blocking pass. 50ms sits between the training chunk //--- (120ms) and the 500ms timer period. #define ARROW_RESTORE_BUDGET_MS 50 //--- Max |diff| between the compute backend and the pure-MQL5 path for a model to be marked //--- MQL5-inference-safe. Summation-order noise is ~1e-6; a genuine port bug shows up as >0.01. #define CPU_INFERENCE_MAX_DIFF 1.0e-3 //--- Equal-frequency bins the feature column is discretised into. MI is biased upward as bins increase, //--- and 8 against MI_SAMPLE_BARS keeps that bias small and EQUAL across candidates - equal is what //--- matters, since this score is only ever used to RANK. //--- FALSE DISCOVERY RATE for the per-column keep report. FDR, not the family-wise bar the headline //--- test uses: FWER asks "is ANY column real" and controls the chance of a single false positive, //--- which is the right question for a verdict and far too conservative for SELECTION - it would //--- discard every genuinely weak-but-useful feature to protect against one false one. Benjamini- //--- Hochberg instead bounds the EXPECTED SHARE of kept columns that are noise, which is what a //--- feature set actually cares about. 0.10 = at most ~10% of what is kept is expected to be junk. #define MI_KEEP_FDR_Q 0.10 #define MI_BINS 8 //--- Bars sampled per candidate. Tuning cost is candidates x this x features, so it is the one number //--- that trades accuracy for time. #define MI_SAMPLE_BARS 2000 #define MI_MIN_SAMPLES 200 //--- Eras the MI diagnostics may wait for the cross-asset panel before reporting without it. #define MI_REPORT_MAX_DEFERRALS 3 //--- How many ERAS the screen may find an unusable sample before it gives up for the run. A COLD //--- start allocates the label cache before it fills it, so BuildMiSample returns 0 usable rows and //--- the screen measures nothing - see ReportFeatureLabelInformation. That is a transient ordering //--- condition, not a verdict, so it must be retried rather than latched. #define MI_REPORT_MAX_ATTEMPTS 8 //--- The sample the screen will WAIT FOR before treating its answer as final, as a fraction of the //--- MI_SAMPLE_BARS target. MI_MIN_SAMPLES is a floor for "can this be computed at all", and using //--- it as the bar for "is this worth keeping" cost the 2026-08-26 fresh start: the screen fired on //--- the first era clearing 200 rows and latched, measuring at 202-773 samples where a warm chart //--- gives ~2065. Columns kept then tracked SAMPLE SIZE, not information - EURUSD kept 0 of 49 at //--- n=202 while SP500 kept 15 at n=773. An underpowered screen that latches is worse than one that //--- waits, because it looks like a result. #define MI_GOOD_SAMPLE_FRACTION 0.60 //--- Capacity re-derive: how many eras a FRESH model stays eligible, and how much the pool must have //--- grown to justify throwing away those eras. The era bound keeps the cost trivial (a re-derive at //--- era <=8 discards almost nothing) and makes a loop impossible to sustain; the growth factor stops //--- a trickle of peer rows from triggering a rebuild that changes no width. #define CAPACITY_RESIZE_MAX_ERA 8 #define CAPACITY_RESIZE_MIN_GROWTH 1.50 //--- Largest |k| the label-alignment scan uses. Padding by |offset| instead shifted the offset //--- build's starting bar and declared every sound measurement void. #define MI_ALIGN_MAX_SHIFT 5 //--- Coordinate-descent passes; the loop breaks as soon as a pass changes nothing, so this is a ceiling. #define MI_TUNE_PASSES 2 //--- Null draws for the observed score. Empirical p cannot go below 1/(B+1), so 200 reports //--- "p<=0.005" and no finer. Cheap: BuildMiSample runs ONCE and every draw reuses it. Counting //--- ranks estimates no spread. #define MI_NOISE_PERMUTATIONS 200 //--- Significance the tuner's winner must reach AFTER correcting for best-of-N. This selector overwrites //--- the user's indicator settings and forces a fresh topology, so a gate has to exist. #define MI_TUNE_ALPHA 0.05 //--- Standard errors a checkpoint's directional precision must clear chance by to be deployable. #define EDGE_MIN_SIGMAS 2.0 //--- BOTH-DIRECTIONS FLOOR. The perceptron reported "Sell:0%" in all 41 of its eras, cleared on Buy //--- alone at 36.6% vs 34%, and deployed. #define DEPLOY_MIN_SIDE_RECALL_PCT 10.0 //--- FAMILY-WISE DEPLOYMENT GATE. EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the //--- MAXIMUM over every era - the one construction this project has repeatedly proven crowns noise. #define DEPLOY_FAMILY_WISE_ALPHA 0.05 //--- Share of bars one class must hold before the era-0 output-bias seed fires. A +-3.0 bias seed is a //--- correction at a 94%-Neutral prior and a distortion at a 50% one. #define COLD_START_SEED_MIN_DOMINANCE 0.70 //--- Reduce-on-regression learning-rate decay. g_eta is read fresh by every weight-update call on //--- every backend, so shrinking it takes effect on the next backProp() everywhere at once. #define ETA_DECAY_REGRESSION_PCT 5.0 // only decay after a real regression, not per-era noise #define ETA_DECAY_FACTOR 0.7 //--- 1e-5, not 1e-4: against the 3e-4 ceiling the old floor left the schedule a 3x dynamic range, so //--- three decays pinned it and "reduce LR on regression" could never settle an oscillating run. The //--- recovery bump still climbs back at 1/0.7 per new best. #define ETA_MIN 0.00001 //--- ComputeFirstLayerWidth() constants. SECONDS_PER_YEAR is the mean Julian year, MQL5's own //--- convention. MARKET_OPEN_FRACTION allows for closed hours and weekends - anything in 0.6-0.85 //--- lands on the same ladder rung. #define SECONDS_PER_YEAR 31557600.0 #define MARKET_OPEN_FRACTION 0.72 //--- Ceiling on how much of the head's LOGIT RANGE the logit-adjustment offsets may consume. Menon //--- et al. assume an UNBOUNDED head. #define LOGIT_ADJUST_MAX_RANGE_FRACTION 0.20 //--- Minimum directional call rate for deployability, as a FRACTION OF THE TRUE DIRECTIONAL BASE RATE //--- rather than an absolute percentage - a model calling a direction a quarter as often as one occurs //--- is sparse but usable; one calling ten times a decade is not, however precise those ten were. #define MIN_COVERAGE_FRACTION_OF_BASE_RATE 0.25 //--- CHECKPOINT BURN-IN. Eras below this may be SCORED and reported but may not become the joint //--- checkpoint, and therefore cannot be deployed or pin the vote threshold. //--- //--- WHY, measured 2026-08-26: XAUUSD deployed the checkpoint from ERA 2 and XTIUSD from ERA 4, each //--- after 69 and 65 further eras failed to beat it. That is not four models agreeing because they //--- learned something - at era 2 they have barely moved off their initialisation and their //--- cold-start priors, so they agree with EACH OTHER almost by construction. Ensemble coverage is a //--- measure of agreement, so it is inflated at exactly the moment the models know least, and it //--- decays monotonically as they differentiate: //--- //--- XAUUSD era 8 6.6% -> era 75 0.4% XTIUSD era 8 9.7% -> era 73 1.4% //--- //--- selectionScore is precision discounted by coverage, so an early era's trivially-high agreement //--- outscores every mature era and the ladder freezes on it. The run then spends its whole budget //--- failing to beat a model that had not trained yet. //--- //--- CONSEQUENCE, AND IT IS INTENDED: a chart whose MATURE coverage cannot clear the floor will now //--- refuse to deploy instead of shipping its era-2 weights. That is the honest outcome - the refusal //--- names coverage, which is the real problem - but it WILL reduce the number of charts that deploy. //--- //--- Heuristic, not derived: the coverage traces above show differentiation largely done by era //--- ~15-20. It is deliberately not tied to a plateau stage, because the ladder's own counters are //--- what this exists to protect. #define ENSEMBLE_CHECKPOINT_MIN_ERA 20 //--- DIRECTIONAL CONFIDENCE THRESHOLD. The decision RULE carries the trading policy rather than //--- distorting the loss (Elkan 2001). Fatal HERE because FitDirConfThreshold branches on the SIGN //--- of (p - break-even), and a memorized curve never shows p < p0, so the get-more-selective //--- branch could never fire. #define DIR_CONF_THRESHOLD_BINS 50 //--- Below this the histogram is too sparse to pick an operating point from. The model then KEEPS THE //--- PREVIOUS ERA'S THRESHOLD rather than falling back to 0.0 - "trade every bar" is the most dangerous //--- setting in the range and must never be what a failed measurement decays to. #define DIR_CONF_MIN_FIT_CALLS 200 //--- Share of the IS span held out to fit the operating point. 15% of ~38k bars is ~5.7k, ~28x the //--- minimum, so the fit is never sparse. Larger buys precision at a direct cost in training data. #define DIR_CONF_CALIB_PCT_OF_IS 15 #define FIRST_LAYER_MIN_WIDTH 16 //--- Conv receptive field in BARS, a structural property of the front-end rather than an input: 3 //--- is the smallest window that can express a turning point (before/at/after), which is what //--- ZigZag marks. #define CONV_RECEPTIVE_FIELD_BARS 3 //--- Sequence-LSTM front-end. 1 = a real recurrence over bars with BPTT, gradient-checked to //--- 2.3e-10 (DirectML\lstm_seq_gradcheck.cpp). 0 = the pre-2026-07-30 single gate step over the //--- flattened input. #define LSTM_SEQUENCE_MODE 1 //--- Where the dense taper ENDS: small enough to force a compressed representation, comfortably wider //--- than the decision itself. The floor below covers the regression head, where 4x1 would be absurd. #define HIDDEN_TAPER_OUTPUT_MULTIPLE 4 //--- 8, NOT the 20 from the MQL5 "4 hidden layers" article - that floor is load-bearing on ITS 1000-wide //--- first layer, and this codebase MEASURES the first layer instead (16 units on live SP500 H4). At 16 //--- a floor of 20 makes lastHidden >= m_initialNeuronsCount, so the WIDTH TAPER NEVER RUNS. #define HIDDEN_TAPER_MIN_WIDTH 8 //--- ComputeHiddenLayerCount() bounds. 2.0 rather than the article's 10/3: halving still produces a //--- taper at the widths derived here. #define HIDDEN_TAPER_TARGET_RATIO 2.0 #define MIN_HIDDEN_LAYERS 2 #define MAX_HIDDEN_LAYERS 5 //--- EstimatedInSampleBars() fallback while history is still downloading - below the trusted-bar floor //--- the measurement says more about the sync state than about the symbol. #define TOPOLOGY_BUDGET_MIN_TRUSTED_BARS 500 #define TOPOLOGY_BUDGET_FALLBACK_YEARS 10 //--- How much the conv stage compresses one bar's feature vector. The layer is a per-bar projection, so //--- filters > features EXPANDS a correlated input at the very bottom of the stack. #define CONV_COMPRESSION_DIVISOR 2 #define CONV_FILTERS_MIN 4 #define CONV_FILTERS_MAX 32 #define LSTM_HIDDEN_MIN 8 #define LSTM_HIDDEN_MAX 128 //--- How far above its own chance rate a best-so-far checkpoint must sit before the regression //--- handler defends it: "still chance-level, keep exploring" versus "a real state we are sliding //--- off", the case that ran unchecked for 228 eras. #define WORTH_DEFENDING_MARGIN_PCT 5.0 //--- PLATEAU LADDER. So: count eras since the last new best and escalate. ANY new best resets //--- counter and stage. #define PLATEAU_PATIENCE_ERAS 8 // eras with no new best selection score before escalating a stage #define PLATEAU_STAGE_RESTART 1 // first boosted warm restart (see PLATEAU_RESTART_BOOST) #define PLATEAU_STAGE_ANNEAL 2 // second boosted warm restart (the gamma anneal it named is gone) #define PLATEAU_STAGE_DEPLOY 3 // exhausted: deploy the best checkpoint and finish the run //--- Restart amplitude. Escaping a basin needs a rate LARGER than the one that settled into it; //--- SGDR restarts span 10-100x, this is tamer because MAX_WEIGHT_DELTA and the checkpoint restore //--- already bound the blast radius. #define PLATEAU_RESTART_BOOST 5.0 //--- HOW MUCH A NEW BEST HAS TO WIN BY, in SEs of the score it is beating. //--- //--- Zero here - a bare `score > best` - is why a run can spend thousands of eras without ever //--- reaching PLATEAU_STAGE_DEPLOY. selectionScore is a win rate over a few hundred independent //--- calls, so it moves several points era to era on noise alone; any upward blip is recorded as a //--- new best, which resets BOTH the counter and the stage, which re-arms a x5 warm restart, which //--- injects fresh noise and produces the next blip. The search sustains itself on its own variance //--- and the ladder never advances. Observed on SP500 H4 2026-08-24: 32.8 / 32.2 / 31.6 / 29.6 / //--- 31.4 across consecutive eras, a ~3-point spread with no trend. //--- //--- 2.0 rather than 1.0 deliberately: the incumbent and the challenger are BOTH noisy estimates, //--- so the SE of their difference is about sqrt(2) x SE, and a 1-SE band was already measured to //--- be too narrow in a noise-dominated search (see the operating-point plateau note). The cost of //--- being too wide is only that the run ratchets less often and finishes sooner, which is the //--- behaviour that was missing. #define PLATEAU_NEW_BEST_SIGMAS 2.0 //--- HOW HARD THE DEPLOY-TIME PASS OVER THE HELD-OUT SLICE PUSHES - see EnableOosFinalPass. It covers //--- the WHOLE slice (13-15k bars measured live), so at the model's own converged rate it is a full //--- training epoch on a model that has already been selected and certified. A quarter step keeps it //--- a refinement. Raise toward 1.0 only with a measured reason. #define OOS_FINAL_PASS_ETA_SCALE 0.25 //--- IN-SAMPLE EARLY STOP. Training error is noisy per era - mini-batch order alone moves it - and //--- ending a run that is still learning costs far more than a few wasted eras. #define IS_ERROR_IMPROVE_FRAC 0.01 #define IS_ERROR_PATIENCE_MULT 3 //--- FILE-COMPATIBILITY SHIMS for three removed inputs. So the old default is still written and //--- hashed, and the .cfg field is no longer COMPARED on load - a model saved under any previous //--- value still loads. #define LEGACY_CONVERGE_WR_SLOT 80 #define LEGACY_STUDY_PERIOD_SLOT 0 #define LEGACY_HISTORY_BARS_SLOT 20 //--- Derived-window rule: median ZigZag leg, snapped DOWN to the ladder, capped. The same walk //--- measures the mean label lifespan the capacity budget divides by - CTopology::MeasureSwingGeometry. #define HISTORY_BARS_FALLBACK 20 #define HISTORY_BARS_FLOOR 6 //--- THE CAPACITY CAP ON THE INPUT WINDOW, AND WHY IT IS A FLEET CONSTANT (2026-08-27). //--- //--- The ZigZag derivation answers "how far back is a swing worth looking", and it says 12. The //--- capacity budget answers "how far back can this much data SUPPORT", and at 49 columns it says 6: //--- ComputeFirstLayerWidth needs width <= ~331 to clear FIRST_LAYER_MIN_WIDTH, and 49 x 12 = 588. //--- Three charts (SP500, XAUUSD, XTIUSD) sat on that floor even after pooling took SP500 from 4.1 to //--- 1.8 weights per independent observation. The window is the min of the two answers. //--- //--- IT CANNOT BE DERIVED PER CHART. Pool rows are keyed on `bars x columns`, so a per-chart window //--- gives each chart its own layout, its own fingerprint and its own pool of one - which is exactly //--- what orphaned SP500 and cost it every peer row it could have had. A capacity cap computed from a //--- chart's own observation count would differ across the fleet by construction. So it is a //--- constant, set from the most starved chart, and every chart shares it. //--- //--- WHY THE LAG AXIS RATHER THAN THE COLUMN AXIS: the corrected lag profile finds NO linear //--- structure at any lag within +/-50, on all six charts, family-wise p=1.0000 - a measured null on //--- the axis being cut. The column-side measures (marginal MI, variance share) are explicitly blind //--- to joint and temporal structure, and the columns they would delete include the whole price core. //--- Cutting where there is a measured null beats cutting where the instrument cannot see. Corroborating: //--- PAI/CONV/LSTM/HYBRID score within ~1pp of each other, so the temporal machinery is not visibly //--- earning the deeper lags. See project_lag_profile_verdict and project_feature_keep_screen. #define HISTORY_BARS_CAPACITY_CAP 6 #define WINDOW_DERIVE_SPAN_BARS 20000 #define WINDOW_DERIVE_MIN_LEGS 30 //--- True OOS samples a class needs before its recall is trusted as a real pass - enough to rule out the //--- zero-sample degenerate case that produced a false convergence at eras 44-46. #define MIN_OOS_CLASS_SAMPLES_FOR_GATE 10 //--- Consecutive regressing eras before the checkpoint is restored and g_eta decayed. Patience is //--- the standard ReduceLROnPlateau formulation and restores that exploration. #define ETA_DECAY_PATIENCE_ERAS 3 //--- Bound on FindConfirmedZigZagPivot()'s backward scan. Generous rather than tight: the scan is plain //--- array reads and its result is cached per bar, so a long scan is paid at most once per unique bar. #define SWING_SCAN_CAP_BARS 750 //--- HOW CLOSE THE TURN HAS TO BE for a bar to be labelled as calling it. A bar is Buy when a swing LOW //--- commits within the next PIVOT_LABEL_TOLERANCE_BARS bars, Sell for a swing HIGH, Neutral otherwise. //--- //--- NOT 1 (the exact next candle), deliberately, and the size is SET BY THE CLASS BALANCE IT //--- PRODUCES rather than by taste. The measured ZigZag leg here is ~20 bars (MeasureSwingGeometry's //--- mean is 1.5*L+0.5, and it logs ~30.9), so pivot density is ~5% per bar and a T-bar window puts //--- roughly 5*T% of bars in the two directional classes combined. //--- //--- WHY THAT MATTERS: ApplyLogitAdjustment's tau is capped at //--- LOGIT_ADJUST_MAX_RANGE_FRACTION * CLASS_LOGIT_SCALE / spread, so the correction it can apply //--- against a dominant Neutral is PINNED AT 1.2 LOGITS whatever the imbalance - the cap binds at //--- every window size. What changes with T is how much imbalance is left over: //--- T=1 5/5/90 needs 2.89, gets 1.20 -> 5.4x residual bias toward Neutral //--- T=2 10/10/80* needs 2.89, gets 1.20 -> 5.4x (*5/5/90, see above) //--- T=5 12/12/75 needs 1.83, gets 1.20 -> 1.9x //--- 5 is the smallest window whose leftover bias the head can plausibly train through. Below it the //--- directional classes risk never firing - the failure this project already paid for once, when the //--- same loss at a comparable ratio produced recall Buy:1% Sell:0% Neutral:100% (1b5a412). //--- //--- Widen this if the directional classes still collapse; narrow it to sharpen entries at a real cost //--- in positives. The prebuild census line prints the ACTUAL per-symbol balance and imbalance ratio - //--- prefer it to the estimate above, which assumes one leg length for every instrument. //--- CHANGING IT CHANGES THE LABEL - the TGT tag in BuildModelFingerprint() interpolates this value //--- for that reason, so a change re-keys every model file instead of silently resuming onto a target //--- the weights were never fitted to. #define PIVOT_LABEL_TOLERANCE_BARS 5 //--- EMA shadow-weight deployment blend rate - see m_shadowNet. 0.01 matches the Tau range used for //--- target-network soft updates in the Gizlyk reference RL algorithms: small enough that no single //--- era's raw weights move the deployed model far, large enough to track sustained learning. #define SHADOW_WEIGHT_TAU 0.01 //--- MINI-BATCH SIZE. 1 restores the exact per-sample SGD this engine had until 2026-08-09, and //--- every helper below is an identity there. Fewer steps need a larger step - sqrt(B) for adaptive //--- methods (Krizhevsky 2014; Granziol et al. #define TRAIN_BATCH_SIZE 8 //--- Floor for Train()'s indicator-depth clamp. Above it a short-but-real history beats livelocking on a //--- depth the terminal will never serve; below it a tiny BarsCalculated() is more likely an indicator //--- mid-calculation than a hard cap, so the clamp stands down. Sized so a clamped era still holds an //--- OOS window worth measuring. #define TRAIN_MIN_CLAMPED_BARS 2000 //--- SettledBars()'s wait. Long enough that a busy terminal makes visible progress between probes, short //--- enough that a chart with nothing to wait for loses only seconds. #define DEPTH_SETTLE_PROBE_MS 3000 #define DEPTH_SETTLE_STABLE_PROBES 3 //--- Hard stop. A depth that has not settled in 10 minutes is not going to, and training on the history //--- that IS there beats waiting forever - the give-up is logged, so it is never confused with a settle. #define DEPTH_SETTLE_TIMEOUT_MS 600000 //--- ERA-BARRIER LIVENESS. How long a member may sit on the same era before the barrier stops //--- treating it as one the others must wait for. See NoteBarrierProgress(). #define ENSEMBLE_BARRIER_STUCK_MS 720000 //--- HARD CAP on how far a member may run ahead of the SLOWEST still-training member, counting one //--- excluded from the barrier. Stopping and naming the laggard beats running and producing //--- nothing. #define ENSEMBLE_MAX_ERA_LEAD 4 //--- How often a held member says so in the journal. The panel line is written every call; this is the //--- durable record, without which a frozen chart leaves no trace at all. #define ENSEMBLE_BARRIER_REPORT_MS 120000 //--- Cadence of the settled per-era diagnostics when VerboseMode is off (see TrainLogDue): each repeating //--- print fires on eras 0-3 and then every Nth. 25 is ~one block per 15 minutes per member at ~35s/era - //--- enough to reconstruct a run without the 22MB/9.5h firehose. CHANGE events are never throttled. #define TRAIN_LOG_EVERY_ERAS 25 //--- Gap between attempts to rebuild a dead indicator handle. Long enough not to hammer a terminal that //--- is genuinely refusing, short enough to recover within one stall-report interval. #define HANDLE_REPAIR_COOLDOWN_MS 30000 //+------------------------------------------------------------------+ //| sqrt(B) learning-rate compensation and the matching patience | //| stretch. Both are exactly 1.0 at B=1, so the whole mini-batch | //| apparatus vanishes when TRAIN_BATCH_SIZE is 1. | //+------------------------------------------------------------------+ double TrainBatchLrScale(void) { return MathSqrt((double)TRAIN_BATCH_SIZE); } int TrainPlateauPatienceEras(void) { return (int)MathRound(PLATEAU_PATIENCE_ERAS * MathSqrt((double)TRAIN_BATCH_SIZE)); } //--- ONLINE CONTINUAL LEARNING (see OnlineLearnStep()). Live-chart-only: a deployed model keeps //--- adapting to newly-RESOLVED bars on the same supervised triple-barrier task, never on trade //--- P&L. #define ONLINE_LEARN_MAX_CATCHUP 64 #define ONLINE_ACC_SMOOTH 50.0 #define ONLINE_LEARN_WARMUP 20 #define ONLINE_LEARN_MIN_ACC 40.0 #define ONLINE_LEARN_ACC_MARGIN 10.0 #define ONLINE_LEARN_PERSIST_EVERY 32 #define ONLINE_LEARN_MAX_CLASS_WEIGHT 5.0 //--- Pinned to the shipped defaults of the removed OversampleParity / ConstrainReplay / FocalLossGamma //--- inputs - see the class-imbalance note above. #define ONLINE_LEARN_PARITY 0.9 #define ONLINE_LEARN_ALPHA_CAP 3.0 #define ONLINE_LEARN_FOCAL_GAMMA 1.0 #define ONLINE_LEARN_ETA_SCALE 0.25 //+------------------------------------------------------------------+ //| Base learning rate for the selected optimizer. | //| A free function, not a method: the constructor's init list needs | //| it for both m_modelEta and m_etaCeiling, which runs before | //| member-init order could safely let one depend on another. | //| The sqrt(B) compensation is applied HERE, at the one point that | //| decides the base rate, so it reaches the ceiling, the plateau | //| boost and the anneal from a single edit. | //+------------------------------------------------------------------+ double InitialEtaForOptimizer(void) { return ((TrainingOptimizer == SGD) ? SgdLearningRate : AdamLearningRate) * TrainBatchLrScale(); } //+------------------------------------------------------------------+ //| Uniform random index in [0, n) for Fisher-Yates shuffles. | //+------------------------------------------------------------------+ int ShuffleRandomIndex(const int n) { return WarriorRandInt(n); } //+------------------------------------------------------------------+ //| ONE Train() CALL'S WORKING STATE. | //| | //| Train() is not a function that trains a model - it is one STEP | //| of a resumable state machine, called again every tick until the | //| era finishes. Everything here is what one step hands to the | //| next: where the era loop got to, whether the era completed, and | //| whether the caller asked it to stop. | //| | //| It exists because these were eight separate locals declared at | //| the top of a 2,300-line function and read throughout all four | //| passes. That is the sole reason no pass could be lifted into a | //| method of its own: each would have needed eight by-reference | //| parameters, and a pass that takes eight is not a pass, it is the | //| same function with a different name. | //+------------------------------------------------------------------+ struct STrainEra { int bars; // bars available to this run int totalIter; // training samples in the in-sample span int oosCutoff; // first index of the out-of-sample span int i; // the era loop's position, preserved across calls bool addLoop; // this era ran to completion (not cut short by the budget) bool stop; // the terminal or the panel asked training to end uint chunkStartTick;// when this call started, for the wall-clock budget uint budgetMs; // max wall-clock work per call before yielding //--- One-shot latch so a failing forward pass reports itself ONCE per call instead of once //--- per sample. bool forwardFailureReported; STrainEra(void) { bars = totalIter = oosCutoff = i = 0; addLoop = stop = forwardFailureReported = false; chunkStartTick = budgetMs = 0; } //--- HAS THIS CALL USED ITS TIME? Every pass yields on the same question, so it is asked in one //--- place. A pass that answers yes must leave its own resume state behind before returning. bool BudgetSpent(void) const { return GetTickCount() - chunkStartTick >= budgetMs; } }; //+------------------------------------------------------------------+ //| ONE ERA'S REPORTABLE NUMBERS, carried from where they are | //| measured to where they are printed. | //| | //| These were twenty-one loose locals declared at the top of Train() | //| and read ~900 lines later, which is the whole reason the era log | //| could not be lifted out of the era loop. -1 means "not measured | //| this era" and prints as n/a: era 0, a stopped era and a cap-hit | //| era all score nothing, and a zero there would read as a real | //| measurement of zero. | //+------------------------------------------------------------------+ struct SEraTelemetry { int buyRecall, sellRecall, neutralRecall; int coverage, dirPrec, chancePrec; int buyPred, sellPred, buyPrec, sellPrec; int buyTrue, sellTrue, neutralTrue, neutralPred; int buyFired, sellFired, neutralFired; int buyFiredPrec, sellFiredPrec; bool shouldLog; // throttle decision, made where the tick count is known SEraTelemetry(void) { Reset(); } void Reset(void) { buyRecall = sellRecall = neutralRecall = -1; coverage = dirPrec = chancePrec = -1; buyPred = sellPred = buyPrec = sellPrec = -1; buyTrue = sellTrue = neutralTrue = neutralPred = -1; buyFired = sellFired = neutralFired = -1; buyFiredPrec = sellFiredPrec = -1; shouldLog = false; } }; //--- AFTER the g_ens* vote globals and the inputs it reads, and after ENUM_SIGNAL: this one is a //--- real class declaration, not a body-only partial, so it is compiled where it stands. #include "Training\OosTally.mqh" //--- AFTER OosTally (it judges one) and the policy #defines it reads. #include "Training\DeployGate.mqh" #include "Training\PooledGate.mqh" //--- Next to PooledGate because it is the same idea one level down: that one pools the DECISION //--- across instruments, this pools the DATA the decision is made from. #include "Training\TrainingPool.mqh" #include "Training\BaselineComparator.mqh" //--- Same reason: needs MAX_PERSISTED_ARROWS/MAX_RESTORED_ARROWS/ARROW_RESTORE_BUDGET_MS, all //--- #defined above, and only CChartView (already fully declared) otherwise. #include "Chart\ChartUI.mqh" //--- Same reason: needs CPU_INFERENCE_MAX_DIFF (#defined above) and //--- CROSSASSET_MAX_PAIRS/ALTDATA_MAX_PIN_CHARS (from System\CrossAsset.mqh/AltData.mqh, both //--- already included near the top of this file), plus only CPersistenceView otherwise. #include "Persistence\ModelPersistence.mqh" //--- Same view+adapter shape again, online-learning side. AFTER every ONLINE_LEARN_*/LABEL_SMOOTH_*/ //--- SHADOW_WEIGHT_TAU #define (all above), which COnlineLearning reads directly (they are macros, //--- not signal members, so no view call carries them). #include "OnlineLearning\IOnlineLearningView.mqh" #include "OnlineLearning\AIBaseOnlineLearningView.mqh" #include "OnlineLearning\OnlineLearning.mqh" //--- Same view+adapter shape again, topology side - but STATELESS, like Persistence: every field //--- BuildModelFingerprint/BuildFreshTopology and the shape-derivation helpers touch is shared //--- elsewhere in the signal (grep-verified). AFTER OnlineLearning (ResetForFreshTopology is one of //--- the two irreducible calls ITopologyView.mqh needs) and every LEGACY_*/TOPOLOGY_BUDGET_*/ //--- WINDOW_*/HISTORY_BARS_*/CONV_*/LSTM_HIDDEN_*/HIDDEN_TAPER_* #define (all above). #include "Topology\ITopologyView.mqh" #include "Topology\AIBaseTopologyView.mqh" #include "Topology\Topology.mqh" //--- Same reasoning as Topology.mqh above (needs CADIndicatorTuner/CCrossAssetPanel, both already //--- included, plus every LEGACY_*/DEPTH_SETTLE_*/HANDLE_REPAIR_*/CROSSASSET_FEATURES #define above). #include "Features\IFeaturesView.mqh" #include "Features\AIBaseFeaturesView.mqh" #include "Features\FeatureBuilder.mqh" //--- BEFORE the class body: CLabelOverlap is used below as a member's TYPE, so it must already be a //--- complete declaration by the time the class is parsed. Reads no member and no input either. #include "Labeling\LabelOverlap.mqh" //--- Same view+adapter shape again, per-config chart lock side - STATEFUL: //--- m_configLockName is exclusive (grep-verified - nothing outside Lifecycle.mqh's old body ever //--- touched it). #include "ConfigLock\IConfigLockView.mqh" #include "ConfigLock\AIBaseConfigLockView.mqh" #include "ConfigLock\ConfigLock.mqh" class CExpertSignalAIBase : public CExpertSignalCustom { protected: string ID; //+------------------------------------------------------------------+ //| ID with the bracketed config tag stripped: "Hybrid 3L [HYB-9369]" | //| -> "Hybrid 3L". The tag tells one CHART's model files from | //| another's, which is a developer's problem, not an owner's. Logs | //| and the verbose panels keep the full ID. Strips from the LAST | //| " [" so a model name containing a bracket cannot truncate more | //| than intended. | //+------------------------------------------------------------------+ string DisplayName(void) const { int cut = StringFind(ID, " ["); int next = cut; while(next >= 0) { cut = next; next = StringFind(ID, " [", cut + 1); } return (cut >= 0 ? StringSubstr(ID, 0, cut) : ID); } //--- Per-MEMBER arrow namespace: "WarSig_PAI_", "WarSig_CONV_", ... Global purges still match on //--- bare "WarSig_". string ArrowPrefix(void) const { return SIG_ARROW_PREFIX + m_id + "_"; } //--- Every subclass of this one is a neural net. Tells the aggregate's raw-arrow layer that this //--- filter draws its OWN arrows, from cached per-bar scans spanning the whole chart, and must not be //--- drawn again from the once-per-bar live path. virtual bool IsAIFilter(void) const override { return true; } //--- m_Open/m_Close/m_High/m_Low/m_Time stay HERE (genuinely shared with Labels.mqh/AutoTune.mqh/ //--- Training.mqh, which read them directly) - CFeatureBuilder reaches them through FeatureOpenAt()/ //--- ChartBarClose()/FeatureHighAt()/FeatureLowAt()/ChartBarTime(). m_Volumes/m_MA/m_RSI/ //--- m_MACDFeature/m_Ichimoku/the AD* indicators below moved onto CFeatureBuilder (m_featureBuilder) //--- as real members - grep-verified exclusive to Features.mqh, touched nowhere else in Expert\. CiOpen m_Open; CiClose m_Close; CiHigh m_High; CiLow m_Low; CiTime m_Time; //--- "Is this AD indicator still calculating?" - cold must be a TRANSIENT rejection, never a //--- zero-fill; see the definition in Expert\Features\FeatureBuilder.mqh. bool ADIndicatorCold(CiCustom &ind, string block) { return m_featureBuilder.ADIndicatorCold(ind, block); } //--- Stamp of the last pass-1 sweep in which EVERY window failed on a transient cause. Non-zero arms //--- a short era-start backoff so the retry loop stops starving the indicator threads it waits on. uint m_coldSweepTick; //--- m_indicatorDepthCapBars/m_indicatorDepthDeadWarned/m_handleRepairTick/m_depthSettleStart/ //--- m_depthProbeTick/m_depthProbeLast/m_depthProbeStable moved onto CFeatureBuilder as real //--- members - grep-verified exclusive to Features.mqh (only Lifecycle.mqh's ctor-init-list //--- touched them elsewhere, which is teardown bookkeeping, not real use). //--- ERA-BARRIER LIVENESS STATE (see ENSEMBLE_BARRIER_STUCK_MS and BarrierEraHeartbeat()). long m_barrierEraSeen; uint m_barrierEraTick; //--- Set by NoteBarrierProgress() when a long one-time phase advances a chunk, so the watchdog can //--- tell "busy" from "stuck" - the distinction it could not make before. bool m_barrierPhaseProgress; bool m_barrierExcluded; uint m_barrierHoldReportTick; //--- One-shot latch for the live-inference hold in RefreshConvergedSignal(). Cleared when the depth //--- returns, so a second outage is reported rather than swallowed. bool m_inferenceDepthRefusalWarned; //--- One-shot latch for the label-prebuild block message: a prebuild that cannot prepare its buffers //--- retries on every scheduled call forever. bool m_prebuildBlockWarned; //--- Ground truth for the training labels - MetaTrader's own Examples\ZigZag. Always created, never //--- gated behind an Enable* input because it is not an optional feature, it IS the label; and never //--- touched by AutoTuneIndicators, because tuning the ground truth alongside the model scored //--- against it would let a trial "improve" by cherry-picking an easier target. CiCustom m_zigZag; //--- Live tunable values for each AD indicator plus their flatten/perturb/best-tracking logic. CADIndicatorTuner m_indicatorTuner; bool m_autoTuneIndicators; //--- Rebuilds only the AD* handles in place, so ReInit picks up updated param structs. bool ReInitTunableIndicators(CIndicators *indicators) { return m_featureBuilder.ReInitTunableIndicators(indicators); } //--- Installs a param set into the tuner and rebuilds handles ONLY when the set actually differs from //--- what the indicators already run - see the definition for the resume-time churn this avoids. bool AdoptIndicatorParams(const double &loaded[], CIndicators *indicators) { return m_featureBuilder.AdoptIndicatorParams(loaded, indicators); } //--- Builds a fresh untrained topology into Net. Split from InitNeuralNetwork() so the tuner can //--- rebuild weights per trial without re-running indicator init, which would Add() them twice. //--- Body in Expert\Topology\Topology.mqh. bool BuildFreshTopology() { return m_topology.BuildFreshTopology(); } //--- Retained so TuneIndicatorsAndTrain() can call ReInitTunableIndicators() between trials. CIndicators *m_indicatorsPtr; CNet *Net; //--- The shadow net, the OOS continual-learning simulation state, the pattern-database backfill //--- state, and the online-learning watermark/guardrail/counters (see OnlineLearnStep()) now //--- default-construct on COnlineLearning (m_onlineLearning, declared below) - see its own //--- constructor and class comment. CArrayDouble *TempData; double dError; double dUndefine; double dForecast; double dPrevSignal; //--- ALTERNATION GATE REMOVED 2026-08-01 with the triple-barrier relabel. m_lastNonNeutralSignal //--- suppressed any live Buy following another Buy with no Sell between. Do not reinstate it. It //--- also meant a one-sided (`Sell:0%`) model got ONE trade per backtest, because the awaited //--- opposite signal that reopens the gate never came. long m_refreshOk; long m_refreshFailFeatures; long m_refreshFailShort; long m_refreshBuy; long m_refreshSell; long m_refreshNeutral; //--- VOTE-GATE census. Without these two the census reads as "the model answers Neutral" - //--- false, and it points at a completely different fix. They separate the model's ANSWER from //--- whether that answer was allowed to become a vote. long m_voteGateBlocked; // directional decisions the readiness gate discarded long m_voteGatePassed; // directional decisions that became a real vote //--- Flag pair as of the first vote attempt, latched so the tally can name WHICH half of the gate //--- failed rather than just reporting that it did. -1 = no vote was ever attempted. int m_voteGateCompleteAtFirst; int m_voteGateLoadedAtFirst; //--- Non-max suppression window for directional signals: keeps the FIRST bar of a same-direction //--- run and drops same-direction neighbours within it, 0 disables. Per-direction, so a missed //--- opposite signal never blocks a later reversal, and causal, so live and drawn history //--- declutter alike. int m_signalClusterWindow; //--- WHAT A KEPT SIGNAL BLOCKS - see SIGNAL_COOLDOWN_SCOPE. Under ANY_SIGNAL the three rules below //--- collapse to ONE: a kept signal of either direction silences everything for the window. Rules //--- 2 and 3 are then not merely redundant but WRONG - alternation would keep blocking a repeat //--- direction forever, long after the cooldown a user asked for had expired. SIGNAL_COOLDOWN_SCOPE m_signalCooldownScope; //--- Per-bar predicted signed signal for THIS era (index = now-relative bar index; -2 = not //--- scored this era). Recording (not drawing) also decouples NMS from pass 2's SHUFFLED order, //--- which no inline cursor could dedup. double m_arrowSignalCache[]; //--- Live-side NMS state: bar TIME of the last SEEN signal per direction (advances on every same- //--- direction bar, kept or suppressed, so a contiguous live run collapses to one) plus the cached //--- accept/suppress decision for that exact bar (keeps repeated same-bar RefreshLatestSignal calls //--- idempotent - re-evaluating the same bar returns its first decision, not a flipped one). 0 = none. datetime m_nmsLiveBuyTime; datetime m_nmsLiveSellTime; bool m_nmsLiveBuyAccept; bool m_nmsLiveSellAccept; //--- Last KEPT live signal of either direction, for cross-direction resolution: a Buy and a Sell //--- within m_signalClusterWindow bars are flicker at one turn zone (real opposite pivots are a //--- whole leg apart), so only the higher-confidence side is kept. datetime m_nmsLiveKeptTime; ENUM_SIGNAL m_nmsLiveKeptDir; double m_nmsLiveKeptConf; datetime dtStudied; //--- TRUE whenever the live net's weights differ from the last successful .nnw save. Set by every //--- mutation path (both Net.backProp sites, both Net.RestoreWeights sites, online learning); //--- cleared only on a successful Net.Save. PersistWeightsOnShutdown skips the ~18MB write when //--- clean - on a terminal close every chart's models used to write at once, and that flood //--- starved two sibling charts' OnDeinit past MetaTrader's budget mid-cleanup (2026-08-25 18:23). //--- Defaults TRUE (unknown state must save); the header's dtStudied watermark going stale on a //--- skip is the same situation as attaching after an offline gap, which the new-bar gate already //--- resolves on the first inference. bool m_netDirty; long m_eraCount; // cumulative era counter, persisted in the .nnw so restarts don't look like they reset progress bool m_trainingComplete; // persisted: true only once Train() converged (objective+stability), not just interrupted bool bEventStudy; //--- This instance's study-event id (STUDY_EVENT_ID_BASE + construction order) and the tick-count //--- when bEventStudy was last armed - see the STUDY_EVENT_ID_BASE comment for why these exist. //--- All arming goes through ArmStudyEvent() so the id and the watchdog stamp can never drift apart. ushort m_studyEventId; uint m_studyArmedTick; //--- out-of-sample holdout: share (%) of the study period never trained on, used only to //--- measure genuine forward accuracy so overfitting shows up in the stats, not just live/OOS trading int m_oosSplitPct; double dOosError; // smoothed OOS mismatch rate (0..100), lower is better double dOosForecast; // smoothed OOS accuracy (0..100) int m_oosSamples; // count of OOS predictions evaluated this Train() call //--- per-era raw (pre-softmax) output-neuron stats over pass 3's OOS scan, reset at pass 3 //--- start; surfaced in the era-end log line. double m_oosOutMin[3]; double m_oosOutMax[3]; double m_oosOutSpreadSum; int m_oosOutCount; //--- WHY "Neutral" WON, per OOS bar. ApplyClassificationSoftmax() requires a STRICT majority and //--- sends every tie to Neutral, so one label covers two events needing OPPOSITE fixes: strict - //--- the net really ranks Neutral highest. long m_oosNeutralStrict; long m_oosNeutralTie; long m_oosTieBuySell; long m_oosRailBars; // any raw output pinned to a sigmoid rail (<=0+eps or >=1-eps) //--- per-era counts of the network's own classification of each bar it fed forward (IS+OOS), //--- reset at the start of every era; surfaced in the status label text so class imbalance //--- (e.g. the network collapsing to all-Neutral) is visible while training runs int m_countBuySignals; int m_countSellSignals; int m_countNeutralSignals; //--- per-era counts of the *true* label of every bar fed forward (IS+OOS), reset alongside the //--- predicted counts above. int m_trueBuyCount; int m_trueSellCount; int m_trueNeutralCount; //--- snapshot of the class totals above, taken at the end of the PREVIOUS era (see Train()'s //--- era-reset block) and held fixed for the whole of the current era. int m_prevEraTrueBuyCount; int m_prevEraTrueSellCount; int m_prevEraTrueNeutralCount; //--- THIS ERA'S OOS CONFUSION COUNTS - all twenty-one of them, cleared and read together. The //--- per-class recalls they imply are the convergence gate that a model "winning" by calling //--- everything Neutral must not pass, and the fired-population rates are what the deploy gate //--- judges. Grouped by LIFETIME: m_oosSamples and dOosError are deliberately NOT in here, being //--- run-level. See Training\OosTally.mqh. SOosTally m_oos; //--- Confidence calibration. EMA-blended across eras so one noisy era cannot swing it, and //--- clamped to [0.3, 1.5] so a degenerate OOS window cannot drive it somewhere absurd. double m_confidenceCalScale; //--- Last era-progress Print for THIS member (see the era loop's 5s rate limit). uint m_lastProgressLogTick; //--- ONE-SHOT DETECTABILITY REPORT: how many calls this configuration must fire before the //--- deploy gate could certify an edge of a given size AT ALL, and what share of the OOS window //--- that is. Purely a report; it gates nothing. void ReportDetectability(int oosBars) { m_featureBuilder.ReportDetectability(oosBars); } //--- m_detectabilityReported moved onto CFeatureBuilder as a real member (exclusive, ctor-init-list //--- only elsewhere). //--- There is deliberately NO minimum-confidence input here any more, and no member holding one. //--- That is what makes ONE input genuinely govern both engines. See ConfidenceTier(). bool m_freezePriorCalibration; bool m_logitAdjustLogged; //--- Latch for the counterpart warning: the correction DECLINING to install. See ApplyLogitAdjustment(). bool m_logitAdjustSkipWarned; //--- True class base rates (natural, un-oversampled), measured from the label distribution each era //--- (UpdateClassPriors, EMA-blended for stability) and PERSISTED alongside the weights (.stats //--- sidecar) so live inference - including after a restart, when no training re-runs - calibrates //--- exactly as training did. 0 = not yet measured => AdjustedSignalFromSoftmax falls back to raw. double m_priorBuy, m_priorSell, m_priorNeutral; //--- The live-fired population (see SOosTally::buyFired), BUCKETED BY CONFIDENCE TIER (ConfidenceTier(), //--- 4 buckets quartiled from the head's structural floor). int m_oosTierFired[4], m_oosTierHits[4]; //--- Set by RankTiersFromOos() the first time this model measures its own tiers on held-out //--- bars. Gates the signal DB out of this filter's pattern weights from then on. bool m_tiersSelfRanked; //--- ERA-END SNAPSHOTS of the arrow cache, taken in RankTiersFromOos() at pass-3 completion - //--- the one moment the cache is complete for the era. double m_overlaySigSnap[]; //--- Deployed-model rebuild state machine (AdvanceDeployedRebuild): 0 = not started / not needed, //--- 1 = rescan in flight, 2 = done. Labels are resolved inline during scoring, NOT prebuilt - //--- see AdvanceDeployedRebuild's header for the windowing bug a prebuild stage caused. int m_deployedRebuildStage; int m_overlaySnapBars; double m_prospectiveSigSnap; //--- HUD display state: the last throttled DISPLAY forward's outputs (softmax probabilities for //--- 3-class heads, the raw scalar in [0] for regression heads), the adjusted signal they //--- resolve to, and the throttle bookkeeping. double m_dispProbs[3]; double m_dispSignal; bool m_dispValid; uint m_dispStamp; long m_dispEra; //--- Which refusal reason the ensemble deploy verdict printed last (1 = never tradeable, //--- 2 = joint checkpoint incomplete, 3 = gate not cleared): a CHANGED reason prints //--- immediately, the same reason repeats only on the TRAIN_LOG_EVERY_ERAS cadence. int m_lastEnsRefusalKey; //--- Cumulative (compounded, persistent) DIRECTIONAL accuracy = the win-rate of the model's //--- Buy/Sell calls: of the bars it actually called Buy or Sell, how many matched the true //--- label. long m_cumIsCorrect, m_cumIsTotal; long m_cumOosCorrect, m_cumOosTotal; //--- Latest live-fired precision (%) and fire count per direction (-1 = n/a), cached at era end for //--- the status panel/log the same way m_lastBuyRecallPct is (see its comment). int m_lastBuyFiredPrecPct, m_lastSellFiredPrecPct; int m_lastBuyFired, m_lastSellFired; //--- ZigZag repainting embargo for the swing-context FEATURES, in bars. The stock ZigZag revises //--- its most recent legs as new bars arrive, so a feature read is trusted only once this many //--- MORE bars have closed after it. The LABEL's lookahead control is the pivot-pair finality //--- rule, not this. int m_swingConfirmationBars; //--- LABEL RESOLUTION LAG in bars: how long after entry this bar's label became KNOWABLE - the //--- earliest bar its pivot pair could have committed on. NOT a diagnostic. See //--- EffectiveSampleSize(). int m_lastLabelLifespan; //--- Companion to m_lastLabelLifespan, written by the same call - see m_labelBarsToPivot. int m_lastLabelBarsToPivot; //--- Running mean of the above over every bar the label cache has resolved this process - see //--- MeanLabelLifespan()/EffectiveSampleSize(). Was two members (a sum and a count) reset from //--- three separate sites; CLabelOverlap owns both and resets in one call - see its declaration. CLabelOverlap m_labelOverlap; //--- DIRECTIONAL CONFIDENCE THRESHOLD - see DIR_CONF_THRESHOLD_BINS for the rationale. Refitted //--- at the end of every pass 2 from that era's own IS margins, because the margin distribution //--- moves with the weights. double m_dirConfThreshold; //--- The value that belongs to the CHECKPOINTED weights. double m_bestDirConfThreshold; //--- Margin histogram for the fit, rebuilt each era from the CALIBRATION slice (see //--- DIR_CONF_CALIB_PCT_OF_IS). long m_dirConfBinCalls[DIR_CONF_THRESHOLD_BINS]; long m_dirConfBinHits[DIR_CONF_THRESHOLD_BINS]; long m_dirConfPrimaryBars; // denominator for coverage: every calibration bar scored //--- one-shot so the "histogram too sparse" explanation is stated once per run, not once per era bool m_dirConfSparseWarned; //--- safety valve: Train()'s do-while loop has no other bound on how many eras it will run //--- before giving up, so a config that can't reach the convergence objective (e.g. too few //--- swing-confirmed examples for the min recall bar to be reachable) would otherwise loop //--- forever, permanently keeping the era-progress status label up instead of the normal per-tick //--- info line and burning CPU nonstop. When the cap is hit, the operator is prompted (see //--- PromptContinuePastEraCap): CONTINUE resets the era counter and keeps training; STOP deploys //--- the best checkpoint found so far (FinalizeTrainRun) and terminates training. Headless //--- (tester/optimizer) runs can't prompt, so they take the STOP branch automatically. int m_maxErasPerRun; //--- Train() runs its per-bar loop synchronously, and MQL5 is single-threaded per chart - a //--- multi-minute era would otherwise starve the terminal's chart-event queue for that whole //--- stretch, including the control panel's own click/drag hit-testing (Panel\ControlPanel.mqh), //--- which depends entirely on CHARTEVENT_MOUSE_MOVE being delivered promptly. bool m_trainRunActive; // true: a run (schedule -> convergence/stop) is in progress, possibly spanning many Train() calls bool m_eraResumePending; // true: yielded mid-bar-loop last call - resume the SAME era, don't start a new one //--- One writer for all five, so a yield point cannot save a partial context. void StashEraResume(const int bars, const int totalIter, const int oosCutoff, const bool add_loop, const int barIndex); int m_resumeBars; int m_resumeTotalIter; int m_resumeOosCutoff; int m_resumeBarIndex; bool m_resumeAddLoop; //--- Bars pass 1 queued as IS-eligible, trained on in pass 2 in a freshly shuffled order rather //--- than pass 1's chronological one. int m_isTrainQueue[]; int m_isTrainQueueCount; //--- NO PARALLEL WEIGHT/PRIMARY ARRAYS. Removed 2026-08-20; the queue is one bar per slot. int m_isTrainCursor; //--- true: pass 1 (sequential) has finished for this era and pass 2 (shuffled backProp) is either //--- running or has yielded mid-queue - Train() skips straight past pass 1's loop on resume when //--- this is set. Reset to false only at a fresh era's start (never mid-run). bool m_isPass2Active; //--- true: pass 2 has already run to natural completion for this era (m_isPass2Active's own //--- false state is ambiguous between "not started yet" and "already finished" - both look //--- identical to a plain `if(!m_isPass2Active)` check). bool m_isPass2Done; //--- Pass 3: chronological, OOS-region-only re-walk that happens AFTER pass 2 has actually trained //--- on this era's IS data - see m_isTrainQueue's declaration comment for why OOS scoring can no //--- longer just happen inline during pass 1 (that would score every era's OOS window against //--- weights from BEFORE this era's training, one full era stale - and for era 0 specifically, //--- against the still-untrained cold-start network, which is why era 0's OOS recall used to show //--- a meaningless 100% Neutral / 0% Buy / 0% Sell every time). Cursor walks i downward from //--- m_oosScoreStartIndex to 0, mirroring pass 1's own iteration bounds/order for whichever bars //--- satisfy isOOS - order matters here (unlike pass 2) since dOosForecast/dOosError are recursive //--- EMAs over the visitation sequence, not order-independent. bool m_isPass3Active; int m_oosScoreIndex; int m_oosScoreStartIndex; //--- Pass 2.5: the CALIBRATION walk. bool m_isCalibActive; bool m_isCalibDone; int m_calibIndex; int m_calibStartIndex; datetime m_lastBarTime; //--- This model's own learning-rate trajectory. g_eta is one file-scope global shared by every //--- CNet in the process, so one member's era-end decay silently changed the rate the OTHER //--- members' next backProp() used - an unintended coupling between independent trajectories. double m_modelEta; //--- Ceiling the era-end recovery bump (Train()'s isBetterEra block) restores `g_eta` toward - //--- used to be the raw AdamLearningRate unconditionally, which is only correct for ADAM. double m_etaCeiling; int m_erasSinceCooldown; // eras completed since the last cooldown reset - replaces the old per-call-only "erasThisCall" CArrayDouble m_oosWindow; // run-scoped OOS stability window (used to be a Train()-local CArrayDouble) double m_bestOosForecast; //--- Selection score (coverage-weighted directional precision) of the era the current checkpoint //--- was taken from. double m_bestSelectionScore; //--- whether the era m_bestOosForecast/the checkpoint was taken from also cleared the gate's //--- deployability floor - part of the "best" ranking itself, not just a side note, so blended //--- accuracy alone can never outrank a deployable era. bool m_bestPassedRecall; //--- Was the checkpointed era calling BOTH directions? Middle tier of the ranking key - see //--- isBetterEra. bool m_bestBothSidesLive; //--- SLOW-ERA HEARTBEAT (2026-08-10). uint m_eraStartTick; ulong m_passFeatUs; // cumulative BuildFeatureWindow time this era, microseconds ulong m_passNetUs; // cumulative feedForward/backProp time this era, microseconds int m_passHeartbeatPrints; uint m_lastHeartbeatTick; //--- How many of pass 1's bars produced a usable feature window, and how many did not. int m_passWindowOk; int m_passWindowFail; //--- Consecutive regressing eras since the last new best - the patience counter for the checkpoint //--- restore / g_eta decay (see ETA_DECAY_PATIENCE_ERAS). Reset by any era that improves. int m_consecutiveRegressions; void TrainHeartbeat(const string tag, int done, int total, const string shortLabel); //--- Progress of the pass currently running, and its name, for the simple panel. int m_passProgressPct; string m_passLabel; //--- STALL REPORTER. Train() is a state machine with several early-return branches ABOVE the era //--- loop (OOS simulation walk, label prebuild, history sync, warm-up, cache invalidation), and //--- every one of them is silent. uint m_lastEraCompleteTick; uint m_lastStallReportTick; void ReportTrainStall(const string branch); //--- THE SIZE OF THE WINDOW AN ERA ACTUALLY TRAINS ON, which nothing reported until 2026-08-24. //--- ReportDetectability and the CAPACITY line both quote EstimatedInSampleBars - derived from the //--- configuration, not from the era - so a window that collapses to a few hundred bars is invisible //--- while every surrounding diagnostic keeps quoting the full history. Reported on CHANGE, because //--- an era over a warm feature cache can finish in a fraction of a second. int m_lastEraWindowBars; void ReportEraWindow(const int barsNow); //--- The era's console line and panel refresh. Self-guarding: it decides nothing, measures //--- nothing and returns immediately when this era had nothing to report, so the era loop no //--- longer has to carry ~200 lines of string building through its own control flow. void ReportEraProgress(const SEraTelemetry &tel); //--- THE FOUR PASSES OF ONE ERA, in the order Train() runs them. Each one guards its own //--- precondition and yields on the shared wall-clock budget, so Train() is the sequence and //--- these are the steps - which is the whole point of STrainEra existing. void RunPass1(STrainEra &era); void RunPass2(STrainEra &era); void RunCalibrationPass(STrainEra &era); void RunOosPass(STrainEra &era); //--- BEFORE ANY OF THAT: does this call belong to training at all? Paused, stopping, deploying, //--- held at the ensemble era barrier, or occupied by one of the three exclusive walks. True = //--- Train() is done for this call. None of it is training, which is exactly why it is not in //--- Train() any more. bool TrainCallPreempted(STrainEra &era); //--- Everything an era does after its last pass scores: calibrate, gate, rank, checkpoint, //--- advance the ladders, persist. Lifted whole because its parts share thirty-odd locals - //--- splitting it further needs an era-outcome object first, not more parameters. void CompleteEra(STrainEra &era, SEraTelemetry &tel); //--- THE TWO SETUPS, on the same contract as TrainCallPreempted: true = this call is spent. //--- Both defer rather than block - a branch that cannot proceed declines the call and lets the //--- next scheduled one try, so the chart's single thread is never slept. bool BeginTrainRun(STrainEra &era, const datetime startTrainBar); // once per run bool BeginEra(STrainEra &era); // once per era, or resume a yielded chunk //--- What pass 1 found, said out loud. Reporting only; self-guarding on era.stop. void ReportPass1Outcome(STrainEra &era); //--- The era completed: count it, age the shadow net, and decide whether the RUN ends here //--- (plateau ladder / ensemble gate, or the operator's answer at the era cap). void AdvanceEra(STrainEra &era, SEraTelemetry &tel); //--- Says WHY this member is idle at the barrier, on the report cadence rather than on entry - //--- a brief hold every era is the design, and printing on entry logged ~950 lines/member/day. void ReportBarrierHold(void); //--- The three exclusive walks each take a whole call. Shared preamble: tell the stall watchdog //--- which branch is running and the era-barrier watchdog that this member is BUSY, not stuck. void ClaimCallForWalk(const string branch); bool m_haveOosCheckpoint; bool m_oosStable; bool m_objectiveMet; //--- RAW inputs to the family-wise deployment gate, snapshotted at the same instant as the checkpoint //--- so the test re-runs on the era that will actually ship rather than on whatever the latest era //--- happened to score. m_bestSelectionScore alone cannot serve: it is precision already multiplied by //--- the coverage credit, and the significance test needs the unweighted precision, the chance rate it //--- is measured against, and the call count that sets its standard error. -1 until the first ranked era. double m_bestDirPrecPct; double m_bestChancePrecPct; int m_bestDirCalls; //--- DECLUSTERED OOS tally: the calls that survive NMS, i.e. the ones that actually become //--- positions now that live NMS gates the trade (see RefreshLatestSignal). Era-scoped, reset //--- with the rest of the OOS counters. int m_oosNmsFired; int m_oosNmsHits; int m_oosNmsLastBuyIdx; int m_oosNmsLastSellIdx; int m_oosNmsKeptIdx; double m_oosNmsKeptConf; ENUM_SIGNAL m_oosNmsKeptDir; //--- How many eras the maximum was taken over - the N in the Sidak correction. Run-scoped: reset //--- with the rest of the best-checkpoint tracking at the top of a fresh run. int m_deployCandidateEras; //--- THE GATE. Re-tests the checkpoint that is about to deploy against the null of the MAXIMUM over //--- m_deployCandidateEras eras, and reports the pieces so the log can show its working. See //--- DEPLOY_FAMILY_WISE_ALPHA. Returns false (refuse) whenever the inputs are missing. bool BestCheckpointSurvivesSelection(double &zObs, double &pFamily, int &nTried); //--- Logs that verdict WITHOUT enforcing it, for the two deploy paths that are explicit operator //--- decisions (the era cap and the panel's Deploy button). Those stay the operator's call; this just //--- makes sure the log never lets an authorised deploy read as a validated one. void ReportSelectionGateVerdict(string context); //--- Plateau ladder state (see the PLATEAU_* constants). m_erasSinceBest counts eras //--- since the last NEW BEST selection score; m_plateauStage is how far up the escalation it //--- has climbed. int m_erasSinceBest; int m_plateauStage; //--- IN-SAMPLE early-stop state (see IS_ERROR_IMPROVE_FRAC). Best training error seen this run and //--- eras since it last improved. -1 = nothing measured yet. double m_bestIsError; int m_erasSinceBestIsError; //--- LATCHES when the IN-SAMPLE error stops improving, and it is a separate flag from //--- m_plateauStage for one measured reason: EnsembleEraVerdict mirrors the shared ladder onto //--- every member with `mm.m_plateauStage = g_ensPlateauStage` on EVERY era, purely so each //--- member's status line reads the collective stage. bool m_isErrorPlateaued; //--- Eras remaining in the current warm-restart boost window (see PLATEAU_RESTART_BOOST): set to //--- PLATEAU_PATIENCE_ERAS by each boosted restart, decremented by the era-end anneal that walks //--- g_eta back to the ceiling, cleared by any new best. int m_restartBoostErasLeft; //--- m_focalGammaRuntime removed 2026-07-31 with focal loss itself - see the removal note at the //--- former m_focalGamma above. The plateau ladder keeps its learning-rate warm restart, which was //--- always the actual escape; the gamma anneal beside it stepped monotonically to zero anyway. uint m_syncWaitStartTick; // 0 = not waiting on history sync; else GetTickCount() when the wait began //--- 3 no-op passes on a fresh start (see InitNeuralNetwork()/ResetWeights()), each its own separately- //--- scheduled Train() call (not a tight in-process loop), so the broker/terminal's history sync gets //--- several real, wall-clock-separated chances to finish before the era loop commits to a bar count. int m_warmupPassesRemaining; //--- ONE-SHOT GUARD for the deploy-time pass over the held-out slice. Set BEFORE the loop runs, so //--- no early return inside it can leave the pass eligible to fire twice on one run. Reset only at //--- the start of a new training run - a retrain is a new selection, so it earns a new pass. bool m_oosFinalPassDone; //--- Newest bar the final pass consumed, 0 = none. Kept so a LATER run can say plainly that its //--- out-of-sample window overlaps bars this model has already trained on, instead of quietly //--- scoring against memorised data. datetime m_oosFinalPassCutoff; bool m_labelCacheBuy[]; bool m_labelCacheSell[]; //--- Bars-to-resolution of each cached label (idx - P2), stored under the SAME validity flag - //--- the pool purge key reads it back as the earliest bar the label could have been known on. int m_labelResolveAge[]; //--- BARS FROM THIS BAR TO THE PIVOT IT CALLS (idx - P1), or -1 where there is no pivot to call. //--- A DIFFERENT QUANTITY from m_labelResolveAge (which is idx - P2) and from the lifespan //--- (PIVOT_LABEL_TOLERANCE_BARS, a constant). Stored under the SAME validity flag as the label. //--- //--- WHY IT IS KEPT: the label fires when a pivot lands up to PIVOT_LABEL_TOLERANCE_BARS bars //--- AHEAD, so on a CORRECT call price may still be moving against the call for d more bars - //--- SwingPivotDirectionLabel says exactly this ("the bars where the turn has not finished coming //--- to us"). Any payoff measured over a window shorter than d is therefore measuring the //--- APPROACH, not the leg, and its negative contribution is expected on the calls that are RIGHT. //--- This is what lets the payoff diagnostic separate the two. int m_labelBarsToPivot[]; bool m_labelCacheHasValue[]; int m_labelCacheBars; // 0 = no cache built yet datetime m_labelCacheAnchorTime; // m_Time.GetData(0) at last (re)build - 2nd invalidation key //--- Resolves and caches the swing label for one bar - finality-gated, see the definition. void AdvanceSwingLabelState(int idx, int bars); //--- Independent-observation count behind `rawN` overlapping labels. See m_lastLabelLifespan for //--- the measurement and for what an uncorrected n did to the operating point. double EffectiveSampleSize(double rawN) const; //--- Mean bars-to-resolution over the label cache, or 1.0 before anything has been measured //--- (which makes EffectiveSampleSize the identity rather than a guess). double MeanLabelLifespan(void) const; //--- Last era's deploy-gate arithmetic, published purely so the era line can state the bar //--- rather than leave it implicit. -1 = not computed this era. double m_lastEdgeFloorPct; double m_lastPrecSE; double m_lastEffN; //--- CROSS-INSTRUMENT CERTIFICATION - see Training\PooledGate.mqh for why the deploy bottleneck //--- is certification rather than training, and why only the EVIDENCE pools while each symbol //--- keeps its own model, geometry and chance rate. A collaborator: it owns a directory of CSV //--- files and knows nothing about a model, so it takes numbers rather than a data view. CPooledGate m_pooledGate; //--- CROSS-INSTRUMENT TRAINING ROWS - see Training\TrainingPool.mqh for the measurement that //--- justifies it (+2.02pp at t_mkt 3.97, clearing its family-wise bar, replicated at D1). //--- Collaborators on the same terms as m_pooledGate: they own a directory of row files and know //--- nothing about a model, so they take numbers rather than a data view. Writer and reader are //--- separate because they run at different points of the era and change for different reasons. CTrainPoolWriter m_trainPoolWriter; CTrainPoolReader m_trainPoolReader; //--- Off unless the operator asks for it. bool TrainPoolEnabled(void) { return Use_Training_Pool; } //--- Gradient-only step for one adopted peer row. Deliberately does NOT touch m_labelCache, the //--- excursion head, the arrow cache or any IS counter: see the dispatch comment in pass 2. void TrainPoolStep(const int poolIdx); //--- Latest time this bar's label could have become knowable, as the pool's purge key. //--- Deliberately the SAME bound the label itself uses - the bar its pivot pair committed on - //--- because a second, approximate model here would drift from the real one, and the whole point //--- of the key is that a peer row must not carry the future into this fit. long TrainPoolResolvedMs(const int entryIdx) { int age = (entryIdx >= 0 && entryIdx < ArraySize(m_labelResolveAge)) ? m_labelResolveAge[entryIdx] : 0; int resolvedIdx = entryIdx - MathMax(age, 1); if(resolvedIdx < 0) resolvedIdx = 0; return (long)m_Time.GetData(resolvedIdx) * 1000; } //--- Fills this instrument's record from its own numbers and hands it over. Stays here because //--- only this class knows its symbol and its label lifespan. void PublishPoolRecord(const double chancePct, const double winPct, const double effN) { SPoolRecord rec; rec.symbol = m_symbol.Name(); //--- _Period, not Period(): inside a CExpertBase subclass the bare call resolves to the //--- inherited SETTER bool CExpertBase::Period(ENUM_TIMEFRAMES) rather than the builtin. rec.timeframe = (int)_Period; rec.chancePct = chancePct; rec.winPct = winPct; rec.effN = effN; rec.lifespanBars = MeanLabelLifespan(); rec.eraCount = (long)m_eraCount; rec.stamp = 0; // stamped by the writer, so every record's clock is one clock m_pooledGate.Publish(ID, rec); } bool PooledGatePasses(string &report) { return m_pooledGate.Passes(report); } //--- Last era's pooled verdict, cached for the era line. Letting a cross-symbol result license a //--- local deploy would ship a model that never cleared its own bar. bool m_lastPoolPasses; string m_lastPoolReport; //--- full per-bar INPUT feature vector cache (everything BufferTempData() computes: ATR- //--- normalized OHLC, time-of-day encoding, volume delta, AD indicator buffers, ...). double m_featureCache[]; bool m_featureCacheHasValue[]; // true once idx has a CACHED SUCCESS (f6150ee: only // successes are ever cached - a miss is never stored, // in any form; see BufferTempData's comment) bool m_featureCacheValid[]; // paired flag, always true when HasValue is true - // kept for the (currently unreachable) cached-miss // shape so the cache layout survives f6150ee //--- Set by BufferTempDataCompute when it rejected a bar because the data had not ARRIVED yet //--- (price buffer EMPTY_VALUE, or an ATR the terminal has not finished calculating) as opposed //--- to the bar being genuinely unusable. Moved onto CFeatureBuilder as a real member (exclusive). //--- WHICH BLOCK rejected the bar, and at which series index - stay HERE (Training.mqh reads both //--- directly for the pass-1 stall report); CFeatureBuilder writes them via FeatureSetFailBlock()/ //--- FeatureSetFailIdx(). string m_featureFailBlock; int m_featureFailIdx; //--- Why the LAST BuildFeatureWindow failed, so the pass-1 stall report can name a cause instead of //--- a count. Slot = which lookback position rejected (-1 = none did and the window was still //--- short); Total = how many values had been assembled when it gave up. Stay HERE for the same //--- reason as m_featureFailBlock; CFeatureBuilder writes them via FeatureSetWindowFail(). int m_windowFailSlot; int m_windowFailTotal; //--- m_featureWidthWarned/m_featureHealthReported moved onto CFeatureBuilder as real members //--- (exclusive, ctor-init-list only elsewhere). //--- One-shot feature-vector autopsy - see ReportFeatureHealth() for the two silent 2026-08-17 //--- failures it exists to catch. Runs the first time pass 1 produces usable windows. void ReportFeatureHealth(int bars) { m_featureBuilder.ReportFeatureHealth(bars); } bool BufferTempDataCompute(int idx) { return m_featureBuilder.BufferTempDataCompute(idx); } //--- Nearest confirmed (non-repainting) ZigZag pivot at or after fromIdx - see this method's //--- definition comment and m_useSwingContext's declaration comment for the repainting-embargo //--- rationale callers must apply to fromIdx before calling this. bool FindConfirmedZigZagPivot(int fromIdx, int &pivotIdx, double &pivotPrice, bool &pivotIsLow); bool EnsureBarCachesCapacity(int bars); bool ShiftBarCaches(const int bars, const int delta); //--- Eager label-cache pre-build + true-label tally, run once per fresh start (see //--- m_warmupPassesRemaining) BEFORE era 0's real training loop begins. bool m_labelCachePrebuilt; // true once the one-time pre-scan has completed bool m_labelPrebuildActive; // true while a chunked pre-scan is in progress bool m_prebuildSeedPending; // true: era 0's era-start reset must NOT stomp the // prebuild-seeded m_prevEraTrue* counts with the // still-empty live tally (see Train()'s era-start block) int m_labelPrebuildBars; int m_labelPrebuildOosCutoff; int m_labelPrebuildIndex; int m_labelPrebuildBuyCount; int m_labelPrebuildSellCount; int m_labelPrebuildNeutralCount; void StartLabelCachePrebuild(void); void AdvanceLabelCachePrebuild(void); //--- Evaluation-only continual-learning OOS simulation and the one-shot pattern-database backfill //--- walk - both now live on COnlineLearning (m_onlineLearning); these stay one-line forwards at //--- their original position so every internal caller (Training.mqh) is unchanged. void StartOosContinualSimulation(int bars, int oosCutoff) { m_onlineLearning.StartOosContinualSimulation(bars, oosCutoff); } void AdvanceOosSimulationChunk(void) { m_onlineLearning.AdvanceOosSimulationChunk(); } void StartPatternDatabaseBackfill(int bars, int totalIter, int oosCutoff) { m_onlineLearning.StartPatternDatabaseBackfill(bars, totalIter, oosCutoff); } void AdvancePatternDatabaseBackfill(void) { m_onlineLearning.AdvancePatternDatabaseBackfill(); } //--- same resumability problem one level up: TuneIndicatorsAndTrain()'s own trial loop calls //--- Train() per trial and used to assume each call ran an entire trial to completion synchronously int m_tuneTrialIndex; // -1 = no multi-trial tuning run in progress double m_tuneBestOosForecast; bool m_tuneLastTrialWasWin; bool m_tuneHaveBestCheckpoint; datetime m_tuneStartTrainBar; //=== Filter-based indicator auto-tuner (see TuneIndicatorsByFilter) ============================= //--- Replaced a genetic + successive-halving search on 2026-08-01. See TuneIndicatorsByFilter() //--- for the measurements and the honest limit. bool m_tuneFilterDone; // the one-shot filter pass has run for this model //--- Mutual information between one feature column and the 3-class label, and the whole-vector score. double FeatureColumnMI(const double &vals[], const int &labels[], int n); //--- Returns the MEAN per-feature marginal MI. "0.001 nats" means nothing on its own; "0.1% of //--- the label's entropy" is a magnitude anyone can act on. double ScoreCurrentParamsByMI(bool shuffleLabels = false); //--- The same work split in two, so the permutation test can extract the sample ONCE and reuse //--- it for every null draw. The sampled range is trimmed by MiShiftPad() at both ends - a FIXED //--- amount, never by |offset| - so every build enumerates the same bars in the same order and //--- two builds can be compared row by row. int BuildMiSample(double &cols[], int &labels[], int labelBarOffset = 0); //--- Bars trimmed from each end of every MI sample. Must cover the largest offset any caller //--- asks for: the alignment scan's MI_ALIGN_MAX_SHIFT and the positive control's quarter of the //--- mean label resolution. int MiShiftPad(void) const { return MathMax(MI_ALIGN_MAX_SHIFT, LabelResolutionBars() / 4); } double ScoreMiSample(const double &cols[], int &labels[], int n, bool shuffleLabels); //--- The permutation test + verdict, split out of the tuner so it is NOT gated on era 0 with it - see //--- the definition. Read-only; runs once per attach, whether or not the sweep did. void ReportFeatureLabelInformation(void); //--- Smallest BarsCalculated() across the ENABLED tunable indicators, or -1 when none is on. The //--- tuner reports this so "the parameter change did not reach the features" can be told apart //--- from "it reached them but they weren't ready". int TunableBarsCalculated(void) { return m_featureBuilder.TunableBarsCalculated(); } //--- Same number, plus HOW MANY tunable indicators were actually consulted. int TunableBarsCalculated(int &enabled) { return m_featureBuilder.TunableBarsCalculated(enabled); } //--- Re-Create any ENABLED tunable indicator whose handle the terminal no longer recognises //--- (BarsCalculated() < 0). Returns true when something was actually rebuilt. bool RepairDeadIndicatorHandles(void) { return m_featureBuilder.RepairDeadIndicatorHandles(); } //--- `want`, clamped to what the indicators can actually serve. THE single gate in front of //--- every ResizeBuffers() call site (train, live inference, chart rescan). int ServableBars(int want, string context) { return m_featureBuilder.ServableBars(want, context); } //--- ServableBars() with a WAIT in front of it, for the paths that can afford one (the training //--- sweep and the label prebuild). Returns >0 = the depth to use, or 0 = "not settled, come //--- back later". int SettledBars(int want, string context) { return m_featureBuilder.SettledBars(want, context); } //--- Per-indicator BarsCalculated(), for the cap/priming/stall lines. It answers that directly. string IndicatorDepthReport(void) { return m_featureBuilder.IndicatorDepthReport(); } //--- One field of that report, and one line of the repair report. Both print the handle NUMBER: //--- a depth of -1 cannot separate "freed under this member" from "created but not calculated yet", //--- and the number can. string IndicatorDepthField(const string name, const int depth, const int handle) { return m_featureBuilder.IndicatorDepthField(name, depth, handle); } int NoteHandleMove(const string name, const int oldHandle, const int newHandle, string &moves) { return m_featureBuilder.NoteHandleMove(name, oldHandle, newHandle, moves); } bool m_miReportDone; //--- Eras the MI report has waited for the cross-asset panel to exist, so it describes the SAME //--- feature vector training uses. Bounded, so a terminal that never syncs the reference symbols //--- still gets its diagnostics rather than silently getting none. int m_miReportDeferrals; //--- Eras on which the screen ran and could not build a usable sample. Distinct from the //--- deferrals above, which count eras it declined to run at all (cross-asset not ready). int m_miReportAttempts; //--- How many cached bars actually carry a resolved label. The MI screen's sample is drawn only //--- from these, so on a cold start the cache can be large and this zero - which is the whole //--- cold-start failure, and printing the two side by side is what makes it self-evident. int LabelCacheResolvedCount(void) const { int n = 0, total = MathMin(m_labelCacheBars, ArraySize(m_labelCacheHasValue)); for(int i = 0; i < total; i++) if(m_labelCacheHasValue[i]) n++; return n; } double m_miBestColumn; double m_miLabelEntropy; //--- PER-COLUMN MI from the most recent ScoreMiSample() call, indexed 0..m_neuronsCount-1. The //--- per-column loop always existed inside ScoreMiSample and threw every value away except the sum //--- and the max; retaining it is what lets the screen say WHICH columns carry the association //--- rather than only that one of them does. double m_miColumn[]; //--- bars between two consecutive MI sample rows, set by BuildMiSample - see its note. int m_miStrideBars; //--- independent label blocks the permutation null was built from (rows within one label resolution //--- move together, so THIS - not the row count - is the sample size the p-value really rests on). int m_miNullBlocks; void TuneIndicatorsByFilter(void); //================================================================================================ void FinalizeTrainRun(void); //--- The "this is now THE model" persistence sequence, shared by every deploy path so they can't //--- drift apart: weights (carrying the current m_trainingComplete flag), the pure-MQL5 //--- inference self-check, the calibration sidecar, and the EMA shadow. void PersistDeployedModel(void); //--- See EnableOosFinalPass. Returns how many bars it trained on; 0 when it did nothing. int OosFinalPass(void); //--- On hitting the per-run era cap: asks the operator whether to keep training (true) or deploy //--- the best checkpoint and stop (false). Headless (tester/optimizer) can't show a dialog, so it //--- returns false. See m_maxErasPerRun's declaration comment. bool PromptContinuePastEraCap(double bestOos); //--- variables //--- training control, driven by the control panel (Warrior_EA.mq5); Train()/OnTickHandler //--- poll these rather than being torn down/rebuilt, so pausing/stopping never loses in-memory state bool m_trainingPaused; // true: Train() blocks between eras until unpaused bool m_trainingStopRequested; // true: OnTickHandler stops scheduling new training passes //--- true for ANY Strategy Tester run - a single backtest AND every optimization pass //--- (MQL_TESTER): the run must NEVER train. Training + online continual learning happen only on //--- a live chart, where this is false. bool m_inferenceOnly; //--- true only when the current Net weights came from a saved .nnw on disk, not from a freshly- //--- built random topology. bool m_modelLoadedFromDisk; //--- Set by EnforceTopologyContract() when a just-loaded .nnw was built by a superseded //--- architecture that cannot be repaired in place (currently: a different conv receptive field, //--- whose weight tensor is a different SHAPE). bool m_topologySuperseded; //--- CAPACITY RESIZE (2026-08-26). What TopologyPooledIndependentBars() returned at the moment //--- this model's first-layer width was derived, and whether the one-shot re-derive has run. //--- //--- ComputeFirstLayerWidth budgets against own bars PLUS the training pool. On a COLD FLEET START //--- every chart derives its topology before any chart has published a pool file - measured //--- 18:13:21 against a first publish at 18:13:48 - so all six size as if training alone and pin //--- that. It is not a rare race: it is what happens EVERY time the feature layout changes, since //--- that invalidates the pool and forces a full wipe. Correcting it by hand needs a two-phase //--- start (run, stop, wipe weights but keep the pool, restart), which is not something an //--- unattended fleet can do for itself. double m_poolObsAtDerivation; bool m_capacityResizeDone; //--- true once ValidateCpuInference() has confirmed this model's pure-MQL5 forward pass matches //--- the compute backend's within tolerance (see CNet::SetCpuInference). Measured at deploy on //--- the chart (where a backend exists to compare against), never in the tester itself. bool m_mqlInferenceValidated; string m_fileName; string m_folderPath; //--- which file Train()'s Net.Save() calls (and this method's own Net.Load()) actually target: //--- the shared FILE_COMMON production weights normally, or a LOCAL per-agent cache file when //--- running inside the Strategy Tester/optimizer (see InitNeuralNetwork) so that repeated //--- optimization passes with an unchanged topology can reuse an already-trained model instead of //--- re-running every era from scratch, without ever touching the live production .nnw/.cfg. string m_activeFileName; bool m_activeFileCommon; //--- user-settable via Inputs.mqh's TrainingOptimizer (SGD or ADAM), read into this member at //--- construction. int m_optimizationAlgo; int m_historyBars; //--- Input-window derivation for a NEW model (existing models adopt theirs from the .cfg): //--- median confirmed swing leg from raw highs/lows - strict local extrema over //--- Median ZigZag leg - the LABEL'S own pivots - snapped down to {12,16,20,24,32}. The same walk //--- measures the mean label lifespan the capacity budget divides by, so this must be called //--- BEFORE ComputeFirstLayerWidth/ComputeLstmHiddenSize, exactly as InitNeuralNetwork orders it. //--- Body in Expert\Topology\Topology.mqh. int DeriveHistoryBars(void) { return m_topology.DeriveHistoryBars(); } int m_outputNeuronsCount; int m_minNeuronsCount; int m_initialNeuronsCount; int m_neuronsCount; double m_neuronsReduction; int m_hiddenLayersCount; //--- LSTM-only recurrent hidden-unit count - see LstmHiddenSize's declaration comment //--- (Variables\Inputs.mqh). Harmless, unused constant contribution to m_fingerprint for MLP/CONV. int m_lstmHiddenSize; //--- CONV-only convolutional output-filter count - see ConvFilterCount's declaration comment //--- (Variables\Inputs.mqh). Harmless, unused constant contribution to m_fingerprint for MLP/LSTM. int m_convFilterCount; int m_minTrainYear; bool m_isInitialized; //--- true once OnDeinit has begun - see MarkShutdown()/FinalizeTrainRun(). bool m_shutdownInProgress; int m_fractalPeriods; //--- The AI's four "market models", in the role a classic signal's geometric m_pattern_N members //--- fill: ConfidenceTierFor() buckets a fire's RAW confidence into one of four equal bands //--- between the head's structural floor (1/3 softmax, 0.5 regression) and 1.0, and the fire //--- votes at that tier's weight. int m_pattern_0, m_pattern_1, m_pattern_2, m_pattern_3; //--- True when this signal runs as one of the ensemble's members - see EnsembleMember(). //--- This class, seen as a CTrainingDataView. Owned by value: it is a pure forwarder with no //--- state of its own beyond the back-pointer, so there is nothing to allocate or free. CAIBaseTrainingData m_trainingData; //--- NON-NN BASELINES on this model's own matrix. A collaborator, not a mixin: it sees the view //--- and nothing else, so it can be read, changed or dropped without touching this class. CBaselineComparator m_baselines; //--- This class, seen as a CChartView. Owned by value for the same reason m_trainingData is. CAIBaseChartView m_chartView; //--- Arrows, the status panel and the HUD line. A collaborator, not a mixin: it sees the view //--- and nothing else - see Expert\Chart\ChartUI.mqh. CChartUI m_chartUI; //--- This class, seen as a CPersistenceView (read+write, unlike CChartView). Owned by value for //--- the same reason m_chartView is. CAIBasePersistenceView m_persistenceView; //--- .cfg/.stats sidecars, CPU-inference validation, net-load retry. STATELESS - see //--- Expert\Persistence\ModelPersistence.mqh's class comment. CModelPersistence m_modelPersistence; //--- This class, seen as a COnlineLearningView. Owned by value for the same reason m_chartView is. CAIBaseOnlineLearningView m_onlineLearningView; //--- Continual learning, the EMA shadow net, the OOS continual-learning simulation and the //--- pattern-database backfill walk - see Expert\OnlineLearning\OnlineLearning.mqh's class //--- comment. STATEFUL, like m_excursionHead: owns the shadow net and every walk's resume state. COnlineLearning m_onlineLearning; //--- This class, seen as a CTopologyView. Owned by value for the same reason m_chartView is. CAIBaseTopologyView m_topologyView; //--- The fingerprint, the derived shape and BuildFreshTopology - see //--- Expert\Topology\Topology.mqh's class comment. STATELESS, like m_modelPersistence. CTopology m_topology; //--- This class, seen as a CFeaturesView. Owned by value for the same reason m_chartView is. CAIBaseFeaturesView m_featuresView; //--- Indicator lifecycle + the per-bar input feature vector - see Expert\Features\FeatureBuilder.mqh's //--- class comment. STATEFUL, like m_excursionHead: owns the 10 feature-only indicator handles //--- (m_Volumes/m_MA/m_RSI/m_MACDFeature/m_Ichimoku/the 5 AD* indicators) and the depth-probe/ //--- handle-repair/spread-series state directly. CFeatureBuilder m_featureBuilder; //--- This class, seen as a CConfigLockView. Owned by value for the same reason m_chartView is. CAIBaseConfigLockView m_configLockView; //--- The per-config chart lock - see Expert\ConfigLock\ConfigLock.mqh's class comment. STATEFUL, //--- like m_excursionHead: owns m_configLockName directly. CConfigLock m_configLock; bool m_ensembleMember; //--- Slot in g_warriorEnsemble (registration order, -1 = not an ensemble member). Doubles as the //--- bit index in the combined-vote masks and the cursor index - see the registry's header comment. int m_ensembleIndex; //--- This member's slot in the combined ensemble panel; claimed lazily on first publish (-1 = none). int m_ensemblePanelSlot; //--- Dedup/throttle for the SOLO status label, mirroring PublishEnsembleStatus's own two guards //--- (StatusLabel.mqh) - the ensemble panel had both, the solo path had neither: SetStatusLabel() //--- was being called unconditionally on every PublishStatus(), unthrottled, on every tick, //--- including inside the tester where nothing is ever drawn. string m_soloStatusLastText; uint m_soloStatusLastRender; //--- functions Creates the OHLC + ZigZag indicators the feature builder reads. Called by //--- InitNeuralNetwork(), never by the framework - the PUBLIC InitIndicators() override below is //--- the framework entry point. bool InitFeatureIndicators(CIndicators *indicators); //--- sets ID/m_id/m_folderPath/m_fileName/m_pattern_count from the subclass constructor - defaults //--- to 4 (the confidence tiers - see m_pattern_0's declaration comment), not 1 void SetIdentity(string id, string shortId, int patternCount = 4); //--- hook for neuron-type-specific layers (Conv+Pool, LSTM, ...); default is a plain perceptron (no-op) virtual bool AddCustomLayers(CArrayObj *topology) { return true; } //--- Reusable front-end stages, composed by the AddCustomLayers() overrides. (They had already //--- drifted - HYBRID guarded the LSTM step with MathMax(1,...) and CSignalLSTM did not, so a //--- historyBars of 1 gave the two a different step.) //--- Bodies in Expert\Topology\Topology.mqh. bool AddConvStage(CArrayObj *topology) { return m_topology.AddConvStage(topology); } bool AddLstmStage(CArrayObj *topology) { return m_topology.AddLstmStage(topology); } //--- Which front-end stages this subclass's AddCustomLayers() actually appends. A virtual rather //--- than a type-enum check, so a future composition cannot silently get the wrong answer. virtual bool UsesConvStage(void) const { return false; } virtual bool UsesLstmStage(void) const { return false; } //--- Labels.mqh: the swing-pivot direction label - THE training target for direction models. ENUM_SIGNAL SwingPivotDirectionLabel(int idx); //--- The one true input width every feedForward guard compares against. int NetInputWidth(void) const { return (int)m_historyBars * m_neuronsCount; } //--- AddConvStage runs BEFORE AddLstmStage wherever both are present (HYBRID), so the LSTM is fed the //--- conv feature map rather than the raw flattened input. bool HasConvBeforeLstm(void) const { return UsesConvStage() && UsesLstmStage(); } //--- Conv chain shape - see the definitions above AddConvStage. Every consumer reads these rather //--- than re-deriving the arithmetic, so the built topology and the logged shape cannot disagree. //--- Bodies in Expert\Topology\Topology.mqh. int ConvReceptiveFieldBars(void) const { return m_topology.ConvReceptiveFieldBars(); } int ConvFirstStagePositions(void) const { return m_topology.ConvFirstStagePositions(); } bool HasSecondConvStage(void) const { return m_topology.HasSecondConvStage(); } int ConvOutputPositions(void) const { return m_topology.ConvOutputPositions(); } int ConvOutputWidth(void) const { return m_topology.ConvOutputWidth(); } //--- Actual input width the LSTM block sees, which is NOT always the flattened input. int LstmFanIn(void) const { return m_topology.LstmFanIn(); } //--- " | conv 21->8 x20 bars | lstm 160->32" for the startup config line; "" when neither applies. string FrontEndConfigSummary(void) const { return m_topology.FrontEndConfigSummary(); } //--- Appends a batch-normalization layer, or does nothing (returning success) when EnableBatchNorm is //--- off. `units` is advisory only - CNet sizes the layer from whatever sits below it, because a conv //--- or pool stage's output width is derived inside the CNet constructor and is not knowable here. //--- See AI\NeuronBatchNorm.mqh for what the layer does and why it exists. //--- Body in Expert\Topology\Topology.mqh. bool AddBatchNormStage(CArrayObj *topology, int units) { return m_topology.AddBatchNormStage(topology, units); } //--- hardcoded activation for the common tapering Dense hidden-layer stack built by //--- BuildFreshTopology() (below AddCustomLayers, above the output layer). virtual ENUM_ACTIVATION HiddenLayerActivation(void) { return PRELU; } //--- Single source of truth for the output head's activation. As two separate literals, changing //--- the head silently did nothing to any existing model. Regression: TANH, whose [-1,1] maps //--- onto the Sell/Neutral/Buy convention. ENUM_ACTIVATION OutputLayerActivation(void) const { return (m_outputNeuronsCount == 1) ? TANH : SIGMOID; } //--- Width of the first dense layer, DERIVED rather than configured. That is ~8 parameters per //--- sample, and it EXPANDS a set of highly correlated inputs instead of compressing them. MUST //--- be called before the fingerprint is built and never again (see the note on fingerprint- //--- feeding members at the top of this file). //--- Body in Expert\Topology\Topology.mqh. int ComputeFirstLayerWidth(void) const { return m_topology.ComputeFirstLayerWidth(); } //--- Expected in-sample training rows for the configured study period, split and timeframe, in //--- INDEPENDENT observations. See the definition for why a raw bar count was the wrong unit to //--- size a network in. Body in Expert\Topology\Topology.mqh. double EstimatedInSampleBars(void) const { return m_topology.EstimatedInSampleBars(); } //--- The same figure BEFORE the overlap deflation, for reports that want to show both. Never size //--- anything from this one - that was the bug. Body in Expert\Topology\Topology.mqh. double EstimatedInSampleBarsRaw(void) const { return m_topology.EstimatedInSampleBarsRaw(); } //--- Width of the vector the first dense layer actually sees: the front-end stage's output where //--- one exists, the flattened window otherwise. Body in Expert\Topology\Topology.mqh. int FirstLayerFanIn(void) const { return m_topology.FirstLayerFanIn(); } //--- Conv output-filter count and LSTM hidden width, DERIVED for the same reason the first-layer //--- width is. Both MUST be called before the fingerprint is built and never again: they assign //--- fingerprint-feeding members (see the note at the top of this file). Bodies in //--- Expert\Topology\Topology.mqh. int ComputeConvFilterCount(void) const { return m_topology.ComputeConvFilterCount(); } int ComputeLstmHiddenSize(void) const { return m_topology.ComputeLstmHiddenSize(); } //--- Dense-taper DEPTH, derived 2026-07-30 from the two endpoints the taper connects. Reads //--- m_initialNeuronsCount, so it MUST be called after ComputeFirstLayerWidth and before the //--- fingerprint - see the note on fingerprint-feeding members at the top of this file. Body in //--- Expert\Topology\Topology.mqh. int ComputeHiddenLayerCount(void) const { return m_topology.ComputeHiddenLayerCount(); } //--- Re-assert everything about a just-loaded net that lives in the FILE but is owned by the CODE. //--- Call after every successful Net.Load(); no-ops (and stays silent) when the file already agrees. void EnforceTopologyContract(void) { m_modelPersistence.EnforceTopologyContract(); } //--- common network bootstrap: indicators, topology build/load, training-file bookkeeping bool InitNeuralNetwork(CIndicators *indicators); //--- The retrain-affecting configuration, as one string whose hash names the .nnw/.cfg pair. //--- Body and the two rules that govern what may enter it: Expert\Topology\Topology.mqh. string BuildModelFingerprint(void) { return m_topology.BuildModelFingerprint(); } //--- Exclusive per-config claim, so two charts can never train into one set of model files. Body //--- on CConfigLock (m_configLock) - see Expert\ConfigLock\ConfigLock.mqh's class comment. bool AcquireConfigLock(void) { return m_configLock.Acquire(); } void ReleaseConfigLock(void) { m_configLock.Release(); } //--- Chart arrows, persistence and the status panel all live in CChartUI now - see //--- Expert\Chart\ChartUI.mqh. These stay as thin forwards, called by name from Training.mqh and //--- this file's own live-tick path; none of their signatures changed. void DrawObject(datetime time, double signal, double close) { m_chartUI.DrawObject(time, signal, close); } void DeleteObject(datetime time) { m_chartUI.DeleteObject(time); } //--- Time-ordered NMS sweep over m_arrowSignalCache: prunes each same-direction run down to its //--- earliest bar (deleting redundant neighbors within m_signalClusterWindow). Run once per era end. void PruneDirectionalClusters(int bars) { m_chartUI.PruneDirectionalClusters(bars); } //--- Whether BOTH directions can currently be traded, which is the precondition for the //--- alternation rule in the NMS paths: with only one side enabled there is no opposite signal //--- to wait for, so requiring alternation would suppress everything after the first call. bool BothDirectionsTradeable(void) const { return true; } //--- Live newest-bar NMS accept test (time-keyed, idempotent per bar time - see m_signalClusterWindow). bool NmsLiveAccept(datetime barTime, ENUM_SIGNAL dir, double conf) { if(m_signalClusterWindow <= 0) return true; if(dir != Buy && dir != Sell) return true; // Idempotent re-eval of the same bar (RefreshLatestSignal can run more than once per bar). if(dir == Buy && m_nmsLiveBuyTime == barTime) return m_nmsLiveBuyAccept; if(dir == Sell && m_nmsLiveSellTime == barTime) return m_nmsLiveSellAccept; long minGap = (long)m_signalClusterWindow * PeriodSeconds(); datetime lastSame = (dir == Buy) ? m_nmsLiveBuyTime : m_nmsLiveSellTime; bool accept; //--- RULE 0 - HARD ANY-DIRECTION COOLDOWN, applied BEFORE and IN ADDITION TO rules 1-3, never //--- instead of them. A kept signal of either direction silences the next `window` bars //--- outright. Suppressing here still advances the per-direction last-SEEN state below, so a //--- run that straddles the cooldown boundary does not restart as if it were fresh. bool coolBlocked = (m_signalCooldownScope == SIGNAL_COOLDOWN_ANY_SIGNAL && m_nmsLiveKeptTime != 0 && (long)(barTime - m_nmsLiveKeptTime) <= minGap); if(coolBlocked) { if(dir == Buy) { m_nmsLiveBuyTime = barTime; m_nmsLiveBuyAccept = false; } else { m_nmsLiveSellTime = barTime; m_nmsLiveSellAccept = false; } return false; } // 1) Same-direction contiguous collapse: suppress if within the window of the previous SEEN // same-direction bar (advance last-seen below either way, so a whole run collapses to one). if(lastSame != 0 && (long)(barTime - lastSame) <= minGap) accept = false; else { // 2) Cross-direction resolution vs the last KEPT opposite signal: keep the stronger side. accept = true; if(m_nmsLiveKeptTime != 0 && m_nmsLiveKeptDir != dir && (long)(barTime - m_nmsLiveKeptTime) <= minGap) { if(conf > m_nmsLiveKeptConf) DeleteObject(m_nmsLiveKeptTime); // this bar is stronger: remove the weaker opposite arrow else accept = false; // the kept opposite is stronger: suppress this bar } //--- 3) ALTERNATION. Rule 1 only collapses a same-direction run inside the window; past //--- it, a second Buy is emitted with no Sell in between, giving Buy/Buy/Buy/Sell. if(accept && BothDirectionsTradeable() && m_nmsLiveKeptTime != 0 && m_nmsLiveKeptDir == dir) accept = false; } if(dir == Buy) { m_nmsLiveBuyTime = barTime; m_nmsLiveBuyAccept = accept; } else { m_nmsLiveSellTime = barTime; m_nmsLiveSellAccept = accept; } if(accept) { m_nmsLiveKeptTime = barTime; m_nmsLiveKeptDir = dir; m_nmsLiveKeptConf = conf; } return accept; } int PurgeChart(void) { return m_chartUI.PurgeChart(); } ENUM_SIGNAL DoubleToSignal(double value); //--- Shared status-label formatting for all three of Train()'s era passes (pass 1 sequential //--- scan/ display, pass 2 shuffled backProp, pass 3 post-training OOS scoring) - see //--- m_isTrainQueue's and m_isPass2Active's declaration comments for why the era loop is now //--- three passes instead of one. void UpdateTrainingStatusLabel(const string &progressLine, double neuron0, double neuron1, double neuron2, double signalValue, bool forceRefresh = false) { m_chartUI.UpdateTrainingStatusLabel(progressLine, neuron0, neuron1, neuron2, signalValue, forceRefresh); } //--- Forced panel refresh from CChartUI's own last-cached values - see its declaration comment. void RefreshStatusLabel(void) { m_chartUI.RefreshStatusLabel(); } //--- The throttle tick and the last-values cache this used to keep now live on CChartUI, next to //--- the panel text they feed - see its declaration comments. //--- Latest OOS Buy/Sell recall (-1 = n/a), read by CChartUI through ChartOosRecallPct() so the panel can show it //--- on every call rather than only at era end. On-chart because blended accuracy is what a trader //--- sees by default, and a model can look good on it purely by calling Neutral often. int m_lastBuyRecallPct, m_lastSellRecallPct; //--- THE single derivation of the 3-class argmax rule - strict majority, ties resolve to //--- Neutral. ApplyClassificationSoftmax()/AdjustedSignalFromSoftmax()/DirectionalMargin() all //--- derive their decision from this one test instead of each re-deriving the comparison. ENUM_SIGNAL Argmax3(double pBuy, double pSell, double pNeutral); //--- Turns the head's 3 SIGMOID values into a softmax distribution in place and returns the //--- signed dPrevSignal convention (+P(buy), -P(sell), exactly 0.0 for neutral). Max-subtracted //--- before exp() for stability. double ApplyClassificationSoftmax(void); //--- Post-hoc logit adjustment / prior correction: reads the raw softmax probabilities //--- ApplyClassificationSoftmax() just left in TempData[0..2] and returns the PRIOR-CORRECTED //--- signed decision (same +P'(buy)/-P'(sell)/0-neutral convention). double AdjustedSignalFromSoftmax(void); //--- Throttled, side-effect-free forward of the CURRENT decision bar for the HUD - body and the //--- full why in AIBase\Inference.mqh. True when m_dispProbs/m_dispSignal hold a usable read. bool DisplayInference(void); //--- Margin between the winning class and its best rival, from the softmax already in TempData. //--- Returns <0 when the winner is Neutral (not a directional call, so no operating point //--- applies) or when the outputs are unreadable. double DirectionalMargin(void); //--- Reset / accumulate / fit, in the order the calibration walk calls them. See //--- DIR_CONF_THRESHOLD_BINS and DIR_CONF_CALIB_PCT_OF_IS. void ResetDirConfHistogram(void); void AccumulateDirConfSample(double margin, bool wasCorrect, bool isPrimaryBar); void FitDirConfThreshold(void); //--- The purge width around every held-out slice: the label's own measured mean resolution lag, //--- because that is how far a label can leak across a split boundary. int LabelResolutionBars(void) const { return (int)MathMax(1.0, MathCeil(MeanLabelLifespan())); } //--- CALIBRATION BAND BOUNDS, in pass-1 bar indices (0 = newest bar, so LARGER index = OLDER). //--- The era's bars lay out, newest to oldest: int CalibPurgeBars(void) const { return LabelResolutionBars(); } int CalibLoIndex(int oosCutoff) const { return oosCutoff + CalibPurgeBars(); } //--- Zero (an empty band) whenever the era is too short to carve one without eating the training set; //--- callers must treat that as "no calibration this era" and leave the threshold where it is. int CalibBandBars(int totalIter, int oosCutoff) const { int isSpan = totalIter - CalibLoIndex(oosCutoff) - CalibPurgeBars(); if(isSpan <= 0) return 0; return (int)(isSpan * (DIR_CONF_CALIB_PCT_OF_IS / 100.0)); } int CalibHiIndex(int totalIter, int oosCutoff) const { return CalibLoIndex(oosCutoff) + CalibBandBars(totalIter, oosCutoff); } //--- EMA-updates the persisted true class base rates (m_priorBuy/Sell/Neutral) from a just-finished //--- era's true class counts. No-op on an empty/degenerate tally. void UpdateClassPriors(long buyCnt, long sellCnt, long neutralCnt); //--- Installs tau*log(prior_c) on Net from the freshly measured priors. Called once per era //--- start, straight after UpdateClassPriors, so the offsets track the same distribution the //--- era is scored against. No-op (and actively clears stale offsets) when the input is off. void ApplyLogitAdjustment(void); //--- Small binary sidecar (fileName + ".stats") persisting the calibration state that must survive a //--- restart for live trading to behave like training: the true class priors and m_confidenceCalScale. bool SaveModelStats(string fileName, bool common) { return m_modelPersistence.SaveModelStats(fileName, common); } bool LoadModelStats(string fileName, bool common) { return m_modelPersistence.LoadModelStats(fileName, common); } //--- Deploy-time (chart, backend present) self-check: runs the just-saved deployed model through //--- both the backend and a temporary pure-MQL5 (CNet::SetCpuInference) clone on the same input //--- window and returns true only if the outputs match within CPU_INFERENCE_MAX_DIFF. bool ValidateCpuInference(void) { return m_modelPersistence.ValidateCpuInference(); } //--- Build the panel's "Buy/Sell accuracy: IS x% OOS y%" line (directional win-rate, Neutral excluded) //--- from the cumulative counts (m_cumIsCorrect etc.); returns "...: measuring..." until at least one //--- directional call has been validated. Shared by the training and live/complete simple panels. string ComputeCompoundedAccuracyLine(void) { return m_chartUI.ComputeCompoundedAccuracyLine(); } //--- Persist/restore the drawn directional arrows (the "WarSig_" objects) to a sidecar file so //--- they survive an EA remove/re-add, recompile, or restart WITHOUT a retrain - the chart //--- objects are destroyed on unload (destructor PurgeChart) and OnInit has no other way to //--- bring them back. bool SaveChartSignals(bool pruneChartObjects = true) { return m_chartUI.SaveChartSignals(pruneChartObjects); } void LoadChartSignals(void) { m_chartUI.LoadChartSignals(); } //--- The shutdown half of that pair: persist, THEN clear the chart, and report both counts. See the //--- definition for why the order is fixed and why the clear is conditional on the write. void PersistAndClearChartSignals(void) { m_chartUI.PersistAndClearChartSignals(); } //--- Wipe this model's drawn arrows AND their .arrows sidecar, plus any deferred restore still in //--- flight. Call from every path that discards or replaces the trained weights - see the definition //--- for why leaving them behind resurrects a dead model's calls through SaveChartSignals. void ClearPersistedChartSignals(const string reason) { m_chartUI.ClearPersistedChartSignals(reason); } //--- Deferred ("async") half of LoadChartSignals: LoadChartSignals only PARSES the sidecar into //--- the arrow-restore buffers (an ~80KB read - instant) and returns, so OnInit never blocks; //--- this then creates the chart objects in ARROW_RESTORE_BUDGET_MS slices, driven by the same //--- 500ms timer that already paces training. void AdvanceChartSignalRestore(void) { m_chartUI.AdvanceChartSignalRestore(); } //--- Deferred ("async") half of StartChartSignalRescan (public, defined inline further down): //--- drains the per-bar inference loop in ARROW_RESTORE_BUDGET_MS slices off PollTraining's //--- timer instead of blocking the button-click handler for however long a full lookback scan //--- takes. void AdvanceChartSignalRescan(void) { m_chartUI.AdvanceChartSignalRescan(); } //--- The arrow-restore queue, the rescan queue/tally, m_lastArrowsSaved and the purge-mismatch //--- latch all live on CChartUI now, next to the methods that own them - see Expert\Chart\ChartUI.mqh. bool ResizeBuffers(int barIndex); bool RefreshData(); bool BufferTempData(int idx) { return m_featureBuilder.BufferTempData(idx); } //--- Assembles the full m_historyBars-wide input window ending AT bar r into TempData, OLDEST //--- BAR FIRST. See the definition comment in Expert\Features\FeatureBuilder.mqh for the //--- measurement behind that. bool BuildFeatureWindow(int r) { return m_featureBuilder.BuildFeatureWindow(r); } //--- shared by OnTickHandler() and the timer-driven PollTraining() - see definition void ScheduleTrainingIfNeeded(void); void Train(datetime StartTrainBar = 0); //--- the training window's start time - shared by Train()'s era start and the label-cache pre-scan datetime TrainWindowStart(datetime startTrainBar); //--- outer loop around Train(): when AutoTuneIndicators is on, tries randomized AD indicator //--- input variations across m_indicatorTuneTrials calls to Train(), keeping the best-OOS one void TuneIndicatorsAndTrain(datetime StartTrainBar = 0); //--- recomputes dPrevSignal/chart arrow for the newest CLOSED bar (bar 1 - see the definition's //--- 2026-08-11 parity comment); used after restoring a checkpointed model at the end of Train() //--- so the live signal matches the deployed weights. bool RefreshLatestSignal(); //--- inference-only "new bar" handler used once m_trainingComplete is true - see //--- ScheduleTrainingIfNeeded()'s declaration comment for why this must NOT call Net.backProp() void RefreshConvergedSignal(void); //--- Online continual-learning step (live chart only), the shadow-net lifecycle, and the sample- //--- weight rule - all now live on COnlineLearning (m_onlineLearning); one-line forwards at their //--- original position. //--- Conservative dirty-marking: the step self-gates and most calls learn nothing, but detecting //--- "actually learned" here would couple this wrapper to the learner's internals. Over-marking //--- costs one redundant autosave per bar for online-learning models - exactly the pre-flag //--- behaviour - while models with the feature off keep a provably clean net. void OnlineLearnStep(void) { if(m_onlineLearning.Enabled() && !m_inferenceOnly && !m_trainRunActive) m_netDirty = true; m_onlineLearning.OnlineLearnStep(); } double OnlineSampleWeight(ENUM_SIGNAL trueSignal, double pBuy, double pSell, double pNeutral) { return m_onlineLearning.SampleWeight(trueSignal, pBuy, pSell, pNeutral); } void EnsureShadowNet(void) { m_onlineLearning.EnsureShadowNet(); } void SaveShadowNet(const double &indicatorParams[]) { m_onlineLearning.SaveShadowNet(indicatorParams); } //--- method of initialization of the indicators. InitOpen/InitClose/InitHigh/InitLow/InitTime/ //--- InitZigZag stay HERE (Expert\AIBase\Features.mqh) - they manage m_Open/m_Close/m_High/ //--- m_Low/m_Time/m_zigZag, which are genuinely shared with Labels.mqh/AutoTune.mqh/Training.mqh //--- (real .GetData() reads there, not just this file), so moving their Create/lifecycle would only //--- relocate a hub behind dozens of pure-relay wrappers - same judgment as Topology's boot sequence. bool InitOpen(CIndicators *indicators); bool InitClose(CIndicators *indicators); bool InitHigh(CIndicators *indicators); bool InitLow(CIndicators *indicators); bool InitVolumes(CIndicators *indicators) { return m_featureBuilder.InitVolumes(indicators); } bool InitTime(CIndicators *indicators); //--- addToCollection=false is used by ReInitTunableIndicators() to rebuild an already-collected //--- handle's params (here: a re-tuned period) without re-adding the (same) pointer into //--- indicators a second time bool InitMA(CIndicators *indicators, bool addToCollection = true) { return m_featureBuilder.InitMA(indicators, addToCollection); } bool InitZigZag(CIndicators *indicators, bool addToCollection = true); //--- common=false targets a LOCAL (non-shared) file - used by the tester/optimizer per-agent //--- weight cache so cross-pass reuse never touches the production FILE_COMMON config/weights. bool SaveTopologyConfiguration(string fileName, int initialNeuronsCount, int hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int historyBars, int outputNeuronsCount, int neuronsCount, int studyPeriod, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int convFilterCount, int lstmHiddenSize, bool common = true) { return m_modelPersistence.SaveTopologyConfiguration(fileName, initialNeuronsCount, hiddenLayersCount, neuronsReduction, minNeuronsCount, optimizationAlgo, historyBars, outputNeuronsCount, neuronsCount, studyPeriod, minTrainYear, isInitialized, stopTrainWR, fractalPeriods, convFilterCount, lstmHiddenSize, common); } //--- The four DERIVED shape fields are by REFERENCE and are ADOPTED from the .cfg, not compared //--- against it. bool LoadAndCompareTopologyConfiguration(string fileName, int &initialNeuronsCount, int &hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int &historyBars, int outputNeuronsCount, int neuronsCount, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int &convFilterCount, int &lstmHiddenSize, bool common = true) { return m_modelPersistence.LoadAndCompareTopologyConfiguration(fileName, initialNeuronsCount, hiddenLayersCount, neuronsReduction, minNeuronsCount, optimizationAlgo, historyBars, outputNeuronsCount, neuronsCount, minTrainYear, isInitialized, stopTrainWR, fractalPeriods, convFilterCount, lstmHiddenSize, common); } //--- CopyFileWithRetry()/CopySharedFile() moved to System\SharedFileCopy.mqh - pure functions, //--- no member ever touched them, so they need no seam on this class at all. Retry helper for the //--- tester/opt seed-copy race: a live chart's own atomic Save() (write .savetmp, then FileMove() //--- over the real file) can hold the source or destination file for a moment, and a concurrent //--- FileCopy/FileOpen from a Strategy Tester agent reading the SAME production file can hit a //--- transient Windows sharing violation in that narrow window. bool LoadNetWithRetry(double &indicatorParams[]) { return m_modelPersistence.LoadNetWithRetry(indicatorParams); } //--- input data bool m_useVolumes; bool m_useTime; bool m_useATR; //--- Uses its own period (m_indicatorTuner.maPeriod), fed as ATR-normalized OHLC distance-from-MA //--- (4 values, same convention as the base close-open/high-open/low-open features) plus the MA's //--- own bar-over-bar change (1 value, ATR-normalized like every other price-domain feature here - //--- not volume's previous-bar-ratio scheme, since a moving average lives in price units and //--- already has ATR as its natural scale reference). See BufferTempDataCompute()'s m_useMA block //--- for the exact 5 values. maPeriod starts equal to the Classic Signals PeriodMA input (see //--- CADIndicatorTuner's constructor) but may diverge from it once AutoTuneIndicators searches a //--- trial - the Classic Signals MA vote itself is untouched by that search, since it needs no //--- training/warm-up and there is nothing for a tuning trial to validate it against. bool m_useMA; //--- Nine normalized swing-context features: 5 confirmed-pivot values plus 4 recent-price-action //--- ones (Donchian position at 20/50 bars, 20-bar return, 20-bar SMA extension) giving fresh //--- context the >=100-bar-stale pivot anchor cannot. Reads the same m_zigZag the labels come //--- from, and is never tuned for the same reason the label side is not. bool m_useSwingContext; //--- see System\NewsRelevance.mqh's declaration comment for what this feature actually encodes //--- (event proximity + impact, not actual-vs-forecast deviation) and why the forward-looking half //--- of it isn't lookahead bias. bool m_useNews; int m_newsFeatureWindowMinutes; //--- Cross-asset panel: the only feature block here whose inputs are NOT a transform of this //--- symbol's own OHLCV series. bool m_useCrossAsset; CCrossAssetPanel m_crossAsset; bool BuildCrossAssetPanel(int bars) { return m_featureBuilder.BuildCrossAssetPanel(bars); } //--- Train->serve parity for the panel (2026-08-11): the pair set is a MEASURED property of the //--- terminal, so like the derived barrier pair it is pinned in the .cfg, not the filename hash //--- (see BuildConfigFingerprint's XA note). string m_crossAssetPairsPinned; bool m_crossAssetCfgSaved; //--- Alternative-data panel (2026-08-16): the second feature block whose inputs are not a //--- transform of this symbol's own series, and the first whose inputs are not derivable from //--- the terminal at all - COT positioning, the VIX complex, macro series, collected and //--- publication-stamped by research/altdata, served as plain CSVs. bool m_useAltData; //--- EnableAltData input, distinct from m_useAltData: the input says the OPERATOR wants the //--- block, m_useAltData says it is actually contributing features (input on AND file present //--- AND >=1 column). bool m_altDataEnabled; //--- One-shot guard for the "data landed after the model was pinned" warning - the upkeep tick //--- runs every 30 minutes and this must not become a recurring line nobody reads. bool m_altDataLateWarned; CAltDataPanel m_altData; string m_altDataNamesPinned; string ReadAltDataPinFromCfg(void) { return m_modelPersistence.ReadAltDataPinFromCfg(); } //--- Spread as a feature. Measured as the strongest single feature in research/test_spread.py, //--- though see the feature block for what it actually encodes and why that is less than it //--- first appears. bool m_useSpreadFeature; //--- m_spreadSeries[]/m_spreadSeriesBars/m_spreadSeriesAnchor/m_crossAssetAnchor moved onto //--- CFeatureBuilder as real members (exclusive, ctor-init-list only elsewhere). bool EnsureSpreadSeries(int bars) { return m_featureBuilder.EnsureSpreadSeries(bars); } public: CExpertSignalAIBase(void); ~CExpertSignalAIBase(void); //--- Reload the alt-data panel after CAltDataFetch rebuilt the feature CSV (OnTimer path, live //--- only). Safe against the per-bar feature cache because new alt rows only ever matter to a //--- NEW D1 bar, which resets that cache anyway. void AltDataReload(void) { //--- Gated on the OPERATOR's switch, NOT on m_useAltData. m_useAltData latches false at init //--- whenever the CSV was absent, so gating the reload on it made the EA structurally unable to //--- consume data IT HAD JUST DOWNLOADED: on the first run after the alt-data folder is wiped - //--- the normal pre-test routine here - the models are built ~30s BEFORE the fetch completes, the //--- reload became a permanent no-op, and the entire run trained on price alone while a complete //--- feature file sat on disk. Measured 2026-08-16 on SP500 H4: models pinned at fingerprint //--- 6de8ba37 (0 alt features) at 19:36:40, SP500_D1.csv rebuilt with 13 features at 19:37:13, //--- and every era after that trained without them - silently, because nothing looked again. if(!m_altDataEnabled) return; int before = m_altData.FeatureCount(); m_altData.Load(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period); int after = m_altData.FeatureCount(); //--- Loading here is INERT while m_useAltData is false (both consumption sites gate on it), //--- so this cannot widen the feature vector out from under a model whose width is already //--- pinned. if(before == 0 && after > 0 && !m_altDataLateWarned) { m_altDataLateWarned = true; Print(ID + ": ALT DATA ARRIVED AFTER THIS MODEL WAS BUILT - " + IntegerToString(after) + " features are on disk now, but this model's input width was pinned WITHOUT them, so it" " is training on price alone and will keep doing so for the rest of this run." " RE-ATTACH THE EA (or reload the chart) to build models that actually train on the" " alt-data block. This is what happens when the alt-data folder is empty at attach time" " and the EA downloads it moments later."); } } //--- THE FRAMEWORK ENTRY POINT, and the only InitIndicators an AI signal needs: every subclass //--- differs in TOPOLOGY (AddCustomLayers), never in how the net is brought up. virtual bool InitIndicators(CIndicators *indicators) override { return InitNeuralNetwork(indicators); } //--- CONTROL PANEL. Every training action the panel offers arrives here, through the filter tree //--- rather than through a registry - see CExpertSignalCustom::OnSignalCommand. virtual bool OnSignalCommand(const ENUM_SIGNAL_COMMAND cmd) override; virtual bool HasSignalTrait(const ENUM_SIGNAL_TRAIT trait) override; //--- "voting" that price will grow/fall, common to every AI signal (single market model) virtual int LongCondition(void); virtual int ShortCondition(void); //--- |dPrevSignal| is already a 0..1 confidence for classification output (softmax probability //--- of the winning class) and typically bounded for regression output (tanh-activated network); //--- OpenParams() clamps regardless. double CalibratedConfidenceMagnitude(void) const { double mag = MathAbs(dPrevSignal); if(!MathIsValidNumber(mag)) return 0.0; if(m_outputNeuronsCount == 3) mag = MathMin(1.0, mag * m_confidenceCalScale); if(!MathIsValidNumber(mag)) return 0.0; return mag; } // Signed for direction-aware use: sign matches dPrevSignal's convention (+ buy, - sell, 0 // neutral/no signal yet). dPrevSignal == -2 is the "not yet studied" sentinel, not a real sell // signal - treat it as no confidence. (The unsigned AIConfidence() override that sat here was // removed 2026-08-25: its only callers were the confidence-scaled trade-management modes.) virtual double SignedAIConfidence(void) override { if(dPrevSignal == -2) return 0.0; double sign = (dPrevSignal > 0.0) ? 1.0 : (dPrevSignal < 0.0) ? -1.0 : 0.0; if(sign == 0.0) return 0.0; return sign * CalibratedConfidenceMagnitude(); } //--- event handlers, common to every AI signal virtual void OnTickHandler(void); //--- drives the same training-scheduling check as OnTickHandler(), but callable from a timer so //--- it isn't dependent on ticks (which don't arrive while the market is closed) void PollTraining(void); virtual void OnChartEventHandler(const int id, const long &lparam, const double &dparam, const string &sparam); //--- methods of adjusting "weights" of the 4 confidence-tier market models - see m_pattern_0's //--- declaration comment void Pattern_0(int value) { m_pattern_0 = value; } void Pattern_1(int value) { m_pattern_1 = value; } void Pattern_2(int value) { m_pattern_2 = value; } void Pattern_3(int value) { m_pattern_3 = value; } virtual void ApplyPatternWeight(int patternNumber, int weight); //--- Re-derives the four tier weights (and the module weight) from THIS era's held-out outcomes. //--- Called once per era at the end of pass 3, when m_oosTierFired/Hits are complete. void RankTiersFromOos(void); //--- ONE FEATURE WINDOW as a plain double[] - the shape both Alglib predictors take, and the //--- one thing the baseline module cannot get from a cache because building it is a live call. //--- Reached through CTrainingDataView::RowFeatures, never named by the module itself. bool BaselineRowFeatures(const int bar, const int width, double &x[]); //--- Direct tier setter, deliberately NOT routed through ApplyPatternWeight(): that override //--- declines writes once self-ranking is live, which is exactly what must not happen to the //--- self-ranker's own writes. Two doors, because they serve opposite purposes. void ApplyTierWeight(const int tier, const int weight) { switch(tier) { case 0: Pattern_0(weight); break; case 1: Pattern_1(weight); break; case 2: Pattern_2(weight); break; default: Pattern_3(weight); break; } } //--- True once this model has measured its own tier win rates on held-out bars. While true the //--- signal DB's ranking is declined for this filter - see ApplyPatternWeight's comment and //--- CExpertSignalCustom::SelfRanked(). virtual bool SelfRanked(void) const override { return m_tiersSelfRanked; } //--- HAS THIS MEMBER DEMONSTRATED ANY SKILL AT ALL? Its own pooled holdout precision against its //--- own chance rate - the same test the deploy gate applies to the ensemble, simply never applied //--- to voting eligibility until now. //--- //--- MEASURED, XTIUSD 2026-08-26: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5% //--- against a 14% chance rate - worse than guessing - and it was still voting. Three healthy //--- members voting Sell scored -21.06/0.77 = -27.4 and cleared the threshold; with the dead one //--- voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its own ensemble on //--- the ~95% of bars where it fired the wrong way, and that WAS the chart's 3.3% coverage. //--- //--- THE MEMBER'S MEASURED PAIR, WITH ITS FALLBACK, IN ONE PLACE - AND THAT IS THE POINT. //--- //--- Both the eligibility test (HasDemonstratedEdge) and the vote itself (LiveVoteContribution) //--- need this member's chance rate, and both used to read m_eraStat* DIRECTLY. Those are //--- ERA-ONLY state: a converged model runs no eras, so after a restart they are -1 and every //--- reader silently degrades. Persisting the certified pair and teaching only ONE of the two //--- readers to fall back to it produced a chart that passed the eligibility test - 4986 of 4999 //--- bars carried a snapshot, the divisor was healthy - and then voted 0.0% on every one of them, //--- because the vote was still subtracting a -1 chance rate and bailing out. Measured on the //--- 2026-08-27 00:41 restart of three freshly deployed charts. //--- //--- One accessor per quantity, so a third reader cannot repeat the mistake. double MeasuredPrecPct(void) const { return (m_eraStatPrecPct >= 0.0) ? m_eraStatPrecPct : m_certifiedPrecPct; } double MeasuredChancePct(void) const { return (m_eraStatChancePct >= 0.0) ? m_eraStatChancePct : m_certifiedChancePct; } //--- Neither existing guard caught it: it IS self-ranked, and its tier weights were 11-14, not 0. bool HasDemonstratedEdge(void) const { if(!m_tiersSelfRanked) return false; //--- No reference rate yet means "cannot judge", which must read as not-yet-eligible rather //--- than as skill. A member with no measurement has demonstrated nothing. double precPct = MeasuredPrecPct(); double chancePct = MeasuredChancePct(); if(chancePct < 0.0 || precPct < 0.0) return false; return (precPct > chancePct); } //--- An AI member's say in the consensus denominator: its module weight once it is ALLOWED to //--- vote (the same readiness test LongCondition gates on), zero before that. virtual double VoteCapableWeight(void) override { //--- No meta test here any more: a gate is not in the voting list at all (IsVotingSignal), //--- so this is never asked of one. That is the point of the split - the special case was //--- load-bearing precisely because a non-voter was in a voters' collection. if(!m_trainingComplete && !(m_inferenceOnly && m_modelLoadedFromDisk)) return 0.0; //--- NO DEMONSTRATED SKILL => NOT IN THE DIVISOR, i.e. ABSENT rather than abstaining. This is //--- the distinction that makes the fix work: an abstainer still contributes its weight to the //--- divisor BY DESIGN (it looked and said nothing, and diluting the consensus is what that //--- should do), so zeroing only a no-skill member's CONTRIBUTION would make the dilution //--- WORSE, not better. It has to leave the denominator too, which is exactly what this //--- function already means for "a member that could not look at all". if(!HasDemonstratedEdge()) return 0.0; return ModuleWeight(); } //--- Same weight, WITHOUT the converged-run requirement - see CExpertSignalCustom's declaration //--- for why the chart reconstruction needs its own. Self-ranking IS still required: an unranked //--- member's LiveVoteContribution is 0 by design (its tier ladder is the constructor's stock //--- 25/50/75/100, which is the wrong unit, not a weak opinion), and putting a 0 contribution in //--- the divisor would dilute the reconstruction with a member that never had an opinion to give. //--- Same skill test as the live divisor, for the same reason: the overlay must picture the vote //--- the EA would actually cast, and a no-skill member is absent from that vote. virtual double ReconstructionWeight(void) override { return HasDemonstratedEdge() ? ModuleWeight() : 0.0; } //--- Gate for the settled PER-ERA diagnostics - see TRAIN_LOG_EVERY_ERAS. HOW OFTEN A DIRECTION //--- ACTUALLY OCCURS, as a percentage of labelled bars. Prints that mark a state CHANGE (new //--- best, stage transition, restore, deploy verdict, warning) must never be put behind this; it //--- exists only for the lines that repeat with the era heartbeat. double ScanDirectionalRatePct(void) const { long tot = (long)m_labelPrebuildBuyCount + m_labelPrebuildSellCount + m_labelPrebuildNeutralCount; if(tot <= 0) return -1.0; return 100.0 * (double)(m_labelPrebuildBuyCount + m_labelPrebuildSellCount) / tot; } double EraDirectionalRatePct(void) const { long tot = (long)m_trueBuyCount + m_trueSellCount + m_trueNeutralCount; if(tot <= 0) return -1.0; return 100.0 * (double)(m_trueBuyCount + m_trueSellCount) / tot; } //--- "3.2x" / "0.4x" / "n/a" for a predicted-vs-true class rate pair. Both are already rounded //--- percentages, so a true rate of 0 has no ratio to report rather than an infinite one. string CalibrationRatio(const int predPct, const int truePct) const { if(predPct < 0 || truePct <= 0) return "n/a"; return StringFormat("%.1fx", (double)predPct / (double)truePct); } bool TrainLogDue(void) const { return VerboseMode || m_eraCount <= 3 || (m_eraCount % TRAIN_LOG_EVERY_ERAS == 0); } //--- What this model would vote on the CURRENT bar if it were deployed. The readiness gate in //--- LongCondition() is what this bypasses, and ONLY for display: dPrevSignal is the decision, //--- deployed or not. virtual bool ProspectiveVote(double &signedVote, double &weight) override { signedVote = 0.0; weight = 0.0; //--- FRESH FORWARD FIRST. DisplayInference() asks the CURRENT weights the live question on a //--- ~4s throttle instead. if(DisplayInference()) { signedVote = LiveVoteContribution(m_dispSignal); weight = ModuleWeight(); return true; } //--- ERA-ARTIFACT FALLBACK CHAIN, newest first - reached only when the fresh forward above //--- cannot run (meta head, warm-up, indicator hole). for(int idx = 1; idx <= 16; idx++) { if(CachedVoteAt(idx, signedVote)) { weight = ModuleWeight(); return true; } } //--- Era-end snapshot next - the fallback that actually fires for ~90% of every era, because //--- the live cache above is wiped at era start and only refills when pass 3 completes. if(m_prospectiveSigSnap != -2.0 && MathIsValidNumber(m_prospectiveSigSnap)) { signedVote = LiveVoteContribution(m_prospectiveSigSnap); weight = ModuleWeight(); return true; } signedVote = 0.0; if(!MathIsValidNumber(dPrevSignal)) return false; signedVote = LiveVoteContribution(dPrevSignal); weight = ModuleWeight(); return true; } //--- The sweep's data source - see CExpertSignalCustom::SnapshotVoteAt for why this is a //--- snapshot and not the live cache. virtual bool SnapshotVoteAt(const int idx, double &signedVote) override { signedVote = 0.0; if(idx < 0 || idx >= m_overlaySnapBars) return false; double sig = m_overlaySigSnap[idx]; if(sig == -2.0 || !MathIsValidNumber(sig)) return false; signedVote = LiveVoteContribution(sig); return true; } virtual bool CachedVoteAt(const int idx, double &signedVote) override { signedVote = 0.0; if(idx < 0 || idx >= ArraySize(m_arrowSignalCache)) return false; double sig = m_arrowSignalCache[idx]; if(sig == -2.0 || !MathIsValidNumber(sig)) return false; signedVote = LiveVoteContribution(sig); return true; } //--- The overlay sweep's truth source - the same inline label resolution ScoreReplayFromCache //--- uses, for the same reason (see its window-mismatch comment). Body in AIBase\Lifecycle.mqh. virtual bool ReplayTruthAt(const int idx, ENUM_SIGNAL &truth) override; //--- buckets the live confidence magnitude into one of the 4 tiers above - see m_pattern_0's //--- declaration comment. Public so PollTraining()/status-display code could surface which tier is //--- currently active if ever useful, though LongCondition/ShortCondition are the only callers today. int ConfidenceTier(void); //--- The same bucketing asked of an ARBITRARY decision value rather than of dPrevSignal. Split out //--- so the OOS scan can ask "what tier would this scanned bar have voted at" - it holds the bar's //--- decision in a local, and dPrevSignal is the LIVE bar's, which is a different bar entirely. int ConfidenceTierFor(const double signal); int PatternWeightForTier(int tier); //--- THE VOTE THIS MEMBER WOULD CAST, in the units CExpertSignalCustom::Direction() actually //--- sums: m_weight (0..1, DB-ranked) x the tier's pattern weight (0..100, DB-ranked), signed + //--- for Buy and - for Sell, and exactly 0.0 when the decision is Neutral (an abstention, which //--- live drops from BOTH the sum and the divisor). double LiveVoteContribution(const double signal); //--- methods of setting adjustable parameters No public setter for m_initialNeuronsCount. An //--- external setter could only ever be called after construction and would either be ignored //--- (if before init) or silently re-key the model mid-run (if after). void OutputNeuronsCount(int value) { m_outputNeuronsCount = value; } //--- No setters for m_hiddenLayersCount / m_lstmHiddenSize / m_convFilterCount: the taper's //--- endpoints are derived, not configured. See BuildFreshTopology()'s taper block. //--- MinSignalConfidence(double) removed with the AI entry floor - confidence now reaches the //--- trade decision as vote weight (ConfidenceTier), gated by the one Min vote to open threshold //--- that the classic votes already answer to. void FreezePriorCalibration(bool value) { m_freezePriorCalibration = value; } void SignalClusterWindow(int value) { m_signalClusterWindow = value; } void SignalCooldownScope(SIGNAL_COOLDOWN_SCOPE v) { m_signalCooldownScope = v; } SIGNAL_COOLDOWN_SCOPE SignalCooldownScope(void) const { return m_signalCooldownScope; } int SignalClusterWindow(void) const { return m_signalClusterWindow; } void SwingConfirmationBars(int value) { m_swingConfirmationBars = value; } int SwingConfirmationBars(void) const { return m_swingConfirmationBars; } //--- ENSEMBLE MEMBERSHIP (two or more direction NNs enabled on one chart). void EnsembleMember(bool value, double voteThreshold = -1.0) { m_ensembleMember = value; if(voteThreshold >= 0.0) g_ensembleVoteThreshold = voteThreshold; if(value && m_ensembleIndex < 0) { int n = ArraySize(g_warriorEnsemble); ArrayResize(g_warriorEnsemble, n + 1); g_warriorEnsemble[n] = GetPointer(this); m_ensembleIndex = n; } } //--- Minimum era among the ensemble members still genuinely training. Falls back to this //--- member's own era when nothing qualifies, which makes the barrier a no-op rather than a //--- lock. long EnsembleMinTrainingEra(void) { long minEra = LONG_MAX; for(int i = 0; i < ArraySize(g_warriorEnsemble); i++) { CExpertSignalAIBase *mm = g_warriorEnsemble[i]; if(CheckPointer(mm) == POINTER_INVALID) continue; if(mm.m_trainingComplete || mm.m_trainingStopRequested || mm.m_trainingPaused || !mm.m_isInitialized) continue; //--- THE LIVENESS EXEMPTION (see ENSEMBLE_BARRIER_STUCK_MS). The three flags above are all //--- VOLUNTARY - a member that chose to stop participating. if(mm.m_barrierExcluded) continue; if(mm.m_eraCount < minEra) minEra = mm.m_eraCount; } return (minEra == LONG_MAX) ? m_eraCount : minEra; } //--- Minimum era among still-training members, IGNORING the barrier exclusion. This is what the lead //--- cap measures against, so an excluded member still BOUNDS the ensemble even though it no longer //--- BLOCKS it - which is the difference between a liveness escape and an unbounded desync. long EnsembleMinEraAnyMember(void) { long minEra = LONG_MAX; for(int i = 0; i < ArraySize(g_warriorEnsemble); i++) { CExpertSignalAIBase *mm = g_warriorEnsemble[i]; if(CheckPointer(mm) == POINTER_INVALID) continue; if(mm.m_trainingComplete || mm.m_trainingStopRequested || mm.m_trainingPaused || !mm.m_isInitialized) continue; if(mm.m_eraCount < minEra) minEra = mm.m_eraCount; } return (minEra == LONG_MAX) ? m_eraCount : minEra; } //--- How many members are actively consuming training chunks right now: still training AND at //--- the barrier's minimum era (a member held ABOVE the min declines its calls, so it costs //--- nothing). int EnsembleActiveTrainers(void) { if(!m_ensembleMember) return 1; long minEra = EnsembleMinTrainingEra(); int active = 0; for(int i = 0; i < ArraySize(g_warriorEnsemble); i++) { CExpertSignalAIBase *mm = g_warriorEnsemble[i]; if(CheckPointer(mm) == POINTER_INVALID) continue; if(mm.m_trainingComplete || mm.m_trainingStopRequested || mm.m_trainingPaused || !mm.m_isInitialized) continue; if(mm.m_eraCount <= minEra) active++; } return MathMax(active, 1); } //--- True when this member has finished more eras than the slowest still-training member and must //--- wait at the era barrier - checked at Train()'s entry (see the barrier note there). bool EnsembleEraBarrierHolds(void) { if(!m_ensembleMember || m_trainingComplete) return false; //--- The ordinary barrier: ahead of the slowest member that is still in it. if(m_eraCount > EnsembleMinTrainingEra()) return true; //--- ...and the backstop for a member that has been EXCLUDED from that minimum. Without this the //--- exclusion is a licence to run away without limit - see ENSEMBLE_MAX_ERA_LEAD. return (m_eraCount - EnsembleMinEraAnyMember() >= ENSEMBLE_MAX_ERA_LEAD); } //--- True when the hold above is the LEAD CAP rather than the ordinary barrier, i.e. we are waiting on //--- a member the barrier has already given up on. Reported differently because the operator's next //--- move differs: an ordinary hold resolves itself, this one needs the named member diagnosed. bool EnsembleLeadCapHolds(void) { return (m_ensembleMember && !m_trainingComplete && m_eraCount <= EnsembleMinTrainingEra() && m_eraCount - EnsembleMinEraAnyMember() >= ENSEMBLE_MAX_ERA_LEAD); } //--- A long one-time phase advanced a chunk. Called from the prebuild / backfill / simulation //--- branches of Train(), which all return before the era loop and so leave m_eraCount untouched //--- for as long as the phase lasts. void NoteBarrierProgress(void) { m_barrierPhaseProgress = true; } //--- Era-advance watchdog for the barrier, kept SEPARATE from m_lastEraCompleteTick on purpose. void BarrierEraHeartbeat(void) { if(!m_ensembleMember) return; uint nowTick = GetTickCount(); if(m_barrierEraTick == 0 || m_barrierEraSeen != m_eraCount) { //--- Progress (or the first observation). Rejoining is unconditional and immediate: a member //--- that just completed an era is by definition not stuck, whatever it was doing before. if(m_barrierExcluded) Print(ID + ": REJOINING THE ERA BARRIER at era " + IntegerToString((int)m_eraCount) + " - it completed an era, so it is training again. It is behind the rest of the" " ensemble, which means it now sets the minimum and the others wait for it to catch" " up. The combined-vote score resumes once every member reports the same era."); m_barrierExcluded = false; m_barrierEraSeen = m_eraCount; m_barrierEraTick = nowTick; return; } //--- BUSY IS NOT STUCK. A member grinding through the label prebuild, the DB backfill or the OOS //--- simulation walk never touches m_eraCount, so the era test above cannot see it working. Any //--- chunk of those phases counts as progress and re-arms the clock, exactly as an era does. if(m_barrierPhaseProgress) { m_barrierPhaseProgress = false; if(m_barrierExcluded) Print(ID + ": REJOINING THE ERA BARRIER - it is still on era " + IntegerToString((int)m_eraCount) + " but is making progress through a one-time preparation phase, not stuck."); m_barrierExcluded = false; m_barrierEraTick = nowTick; return; } //--- Same era as last look. Only a member that is AT the minimum can be the one blocking: a member //--- ahead of it is not advancing because the barrier is holding it, which is correct behaviour and //--- must never be mistaken for being stuck. if(m_barrierExcluded || m_eraCount > EnsembleMinTrainingEra()) return; if(nowTick - m_barrierEraTick < ENSEMBLE_BARRIER_STUCK_MS) return; m_barrierExcluded = true; PrintFormat("%s: RELEASING THE ERA BARRIER - this member has not completed an era in %.0f minutes" " (still at era %d) and every other member on this chart has been waiting on it for" " that entire time. It is excluded from the barrier minimum so the rest can advance;" " it keeps training and rejoins the moment it finishes an era. READ THE TRAIN STALL" " LINE ABOVE for why it is not finishing - the barrier only reports that it is stuck," " never why. NOTE: while the ensemble is desynchronised the combined-vote OOS score" " cannot be computed (it scores only bars EVERY member contributed at the same era)," " so no ensemble verdict will be published until this member catches up.", ID, (nowTick - m_barrierEraTick) / 60000.0, (int)m_eraCount); } //--- WHAT A CALL ON THIS BAR WAS WORTH, before any exit policy: the forward close move and the //--- two extreme excursions over the next K bars, each divided by the bar's own ATR so they are //--- comparable across instruments and across volatility regimes. //--- //--- SERIES INDEXING: bar 0 is the NEWEST, so forward in time is a SMALLER index. The newest //--- `horizon` bars of the OOS slice have no forward window at all and report unmeasurable. //--- See the g_ensVoteFwdR declarations for why this is called at TWO horizons. int PayoffHorizonShort(void) { return PIVOT_LABEL_TOLERANCE_BARS; } int PayoffHorizonHold(void) { return PIVOT_LABEL_TOLERANCE_BARS + (int)MathRound(m_topology.SwingLegMedianBars()); } void MeasureBarPayoff(const int barIdx, const int horizonIn, double &fwdR, double &upR, double &dnR, bool &haveR) { fwdR = upR = dnR = 0.0; haveR = false; int horizon = horizonIn; if(horizon < 1) horizon = 1; //--- No forward window: the leading edge of the series, not a zero move. if(barIdx < horizon) return; double atr = m_ATR.Main(barIdx); if(!MathIsValidNumber(atr) || atr <= 0.0) return; double entry = m_Close.GetData(barIdx); if(!MathIsValidNumber(entry) || entry <= 0.0) return; double hi = entry, lo = entry; for(int j = barIdx - 1; j >= barIdx - horizon; j--) { double h = m_High.GetData(j), l = m_Low.GetData(j); //--- A hole in the window makes the EXTREMES wrong, not merely noisy, so the whole row is //--- abandoned rather than measured over a shortened span. if(!MathIsValidNumber(h) || !MathIsValidNumber(l) || h <= 0.0 || l <= 0.0) return; if(h > hi) hi = h; if(l < lo) lo = l; } double close = m_Close.GetData(barIdx - horizon); if(!MathIsValidNumber(close) || close <= 0.0) return; fwdR = (close - entry) / atr; upR = (hi - entry) / atr; dnR = (entry - lo) / atr; haveR = true; } //--- Pass-3 hook: record the VOTE this member would have cast on this OOS bar into the combined- //--- vote buffer. signedVote is in live vote units - m_weight x tier pattern weight, signed by //--- direction (see LiveVoteContribution()) - NOT the raw confidence this used to carry. void EnsembleOosContribute(const int barIdx, const double signedVote, const double voteWeight, const bool labelBuy, const bool labelSell, const bool dirLabel) { if(!m_ensembleMember || m_ensembleIndex < 0 || m_ensembleIndex >= 8) return; datetime t = m_Time.GetData(barIdx); if(t <= 0) return; if(g_ensVoteEra != m_eraCount) { //--- first contribution of a new era resets the buffer (the era barrier keeps members aligned, //--- so a mismatched stamp means "previous era's rows", never "a sibling's different era") g_ensVoteEra = m_eraCount; g_ensVoteRows = 0; g_ensVoteDoneMask = 0; ArrayInitialize(g_ensVoteCursor, 0); } int bit = (1 << m_ensembleIndex); //--- monotonic cursor first (members scan bars oldest-to-newest, so the match is O(1) amortized), //--- full wrap-around only when per-member window failures desynchronize the sequences int row = -1; int start = g_ensVoteCursor[m_ensembleIndex]; if(start > g_ensVoteRows) start = 0; for(int i = start; i < g_ensVoteRows; i++) if(g_ensVoteTime[i] == t) { row = i; break; } if(row < 0) for(int i = 0; i < start; i++) if(g_ensVoteTime[i] == t) { row = i; break; } if(row < 0) { if(g_ensVoteRows >= ArraySize(g_ensVoteTime)) { int cap = g_ensVoteRows + g_ensVoteRows / 2 + 512; ArrayResize(g_ensVoteTime, cap); ArrayResize(g_ensVoteSum, cap); ArrayResize(g_ensVoteMember, cap * ENS_MAX_MEMBERS); ArrayResize(g_ensVoteMask, cap); ArrayResize(g_ensVoteVoterMask, cap); ArrayResize(g_ensVoteWeightSum, cap); ArrayResize(g_ensVoteLabelBuy, cap); ArrayResize(g_ensVoteLabelSell, cap); ArrayResize(g_ensVoteDirLabel, cap); ArrayResize(g_ensVoteFwdR, cap); ArrayResize(g_ensVoteUpR, cap); ArrayResize(g_ensVoteDnR, cap); ArrayResize(g_ensVoteHasR, cap); ArrayResize(g_ensVoteFwdR2, cap); ArrayResize(g_ensVoteUpR2, cap); ArrayResize(g_ensVoteDnR2, cap); ArrayResize(g_ensVoteHasR2, cap); ArrayResize(g_ensVoteD, cap); } row = g_ensVoteRows++; g_ensVoteTime[row] = t; g_ensVoteSum[row] = 0.0; for(int mm = 0; mm < ENS_MAX_MEMBERS; mm++) g_ensVoteMember[row * ENS_MAX_MEMBERS + mm] = 0.0; g_ensVoteMask[row] = 0; g_ensVoteVoterMask[row] = 0; g_ensVoteWeightSum[row] = 0.0; //--- the label comes from the shared label cache, so it is identical across members - //--- whichever member reaches the bar first writes it g_ensVoteLabelBuy[row] = labelBuy; g_ensVoteLabelSell[row] = labelSell; g_ensVoteDirLabel[row] = dirLabel; //--- Same "whichever member reaches the bar first writes it" rule as the label above: the //--- forward excursion is a property of the CHART, identical for every member, so measuring //--- it once per ROW rather than once per member per row keeps it off the per-member path. g_ensVoteD[row] = LabelBarsToPivot(barIdx); MeasureBarPayoff(barIdx, PayoffHorizonShort(), g_ensVoteFwdR[row], g_ensVoteUpR[row], g_ensVoteDnR[row], g_ensVoteHasR[row]); MeasureBarPayoff(barIdx, PayoffHorizonHold(), g_ensVoteFwdR2[row], g_ensVoteUpR2[row], g_ensVoteDnR2[row], g_ensVoteHasR2[row]); } if((g_ensVoteMask[row] & bit) != 0) return; // already contributed to this bar this era (defensive - a re-run must not double-count) g_ensVoteSum[row] += signedVote; if(m_ensembleIndex >= 0 && m_ensembleIndex < ENS_MAX_MEMBERS) g_ensVoteMember[row * ENS_MAX_MEMBERS + m_ensembleIndex] = signedVote; g_ensVoteMask[row] |= bit; //--- VOTER, not merely present. -0.0 compares equal to 0.0, so an abstention that arrived //--- with a negative zero is still correctly excluded here. g_ensVoteWeightSum[row] += voteWeight; if(signedVote != 0.0) g_ensVoteVoterMask[row] |= bit; g_ensVoteCursor[m_ensembleIndex] = row + 1; } //--- This member's just-finished era, held until the ensemble verdict can act on it. Same //--- quantities the solo gate keeps in m_best*. double m_eraStatPrecPct; double m_eraStatChancePct; //--- THE LADDER'S OWN VERDICT ON ITSELF, set by RankTiersFromOos from the SAME pooled holdout the //--- tier weights come from. It exists because the era pair above is era-only state: a CONVERGED //--- model runs no eras, so after a restart it had no measurement at all and HasDemonstratedEdge() //--- ruled it no-skill. This one is written by every path that ranks a ladder - the era end AND the //--- deployed replay - and is persisted (WST8), so a resumed model knows whether it may vote. double m_certifiedPrecPct; double m_certifiedChancePct; int m_eraStatCalls; bool m_eraStatTradeable; bool m_eraStatTwoSided; double m_eraStatScore; double m_eraStatBlended; double m_eraStatThreshold; //--- Which era this member's in-memory snapshot belongs to (-1 = none). long m_checkpointEra; void EnsembleStashEraStats(const double precPct, const double chancePct, const int calls, const bool tradeable, const bool twoSided, const double score, const double blended) { m_eraStatPrecPct = precPct; m_eraStatChancePct = chancePct; m_eraStatCalls = calls; m_eraStatTradeable = tradeable; m_eraStatTwoSided = twoSided; m_eraStatScore = score; m_eraStatBlended = blended; //--- the operating point belongs with the weights it was fitted for - see m_bestDirConfThreshold m_eraStatThreshold = m_dirConfThreshold; } //--- Called once per era from the era-end block, after this member's statistics are final. //--- Defined in Training.mqh - it needs the PLATEAU_* machinery. void EnsembleOosPassComplete(const long votedEra, double &etaLocal); //--- The verdict itself, and its pieces. needMask names the members whose reads the vote is built //--- from (still-training members only - a paused or deployed member is not voting in training). void EnsembleEraVerdict(const int needMask, const long votedEra, double &etaLocal); void EnsembleCommitJointCheckpoint(const long votedEra); //--- Does the best combined-vote era survive having been CHOSEN out of g_ensCandidateEras eras? //--- Identical construction to BestCheckpointSurvivesSelection, applied to the vote. bool EnsembleSurvivesSelection(double &zObs, double &pFamily, int &nTried); //--- Single choke point for this signal's on-chart status text. Every AI-side SetStatusLabel //--- call site routes through here so no mode can regress into four stacked panels. void PublishStatus(const string text, const bool force = false) { if(!m_ensembleMember) { //--- THE LIVE AGGREGATED VOTE, appended here rather than by every caller of PublishStatus - //--- see g_liveVoteLine's declaration (ExpertSignalCustom.mqh) for what replaced it. string full = (g_liveVoteLine != "") ? text + "\n" + g_liveVoteLine : text; //--- Same two guards PublishEnsembleStatus already applies: unchanged text needs no //--- relayout, and changed text still respects a minimum redraw interval. SetStatusLabel() //--- word-wraps, measures and re-sets several chart objects then calls ChartRedraw() - real //--- work this was paying for on every tick regardless of whether anything visible changed. uint now = GetTickCount(); if(!force && full == m_soloStatusLastText) return; if(!force && m_soloStatusLastRender != 0 && now - m_soloStatusLastRender < 300) return; m_soloStatusLastText = full; m_soloStatusLastRender = now; SetStatusLabel(full); return; } //--- Called EVERY publish, not just the first. The old "claim once, first publisher wins the //--- next free row" form is what ordered the panel by who was busiest instead of by member //--- index. m_ensemblePanelSlot = ClaimEnsemblePanelSlot(DisplayName(), m_ensembleIndex); int nl = StringFind(text, "\n"); PublishEnsembleStatus(m_ensemblePanelSlot, (nl > 0) ? StringSubstr(text, 0, nl) : text, force); } void EnableOnlineLearning(bool value) { m_onlineLearning.SetEnabled(value); } void MaxErasPerRun(int value) { m_maxErasPerRun = value; } void OOSSplit(int value) { m_oosSplitPct = value; } //--- No setters for m_historyBars / m_minTrainYear. The window is DERIVED at InitNeuralNetwork or //--- ADOPTED from the .cfg (see DeriveHistoryBars); the year floor is a constructor constant. Both //--- remain members only because the .cfg field layout is positional. void UseVolumes(bool value) { m_useVolumes = value; } bool UseVolumes(void) const { return m_useVolumes; } void UseTime(bool value) { m_useTime = value; } bool UseTime(void) const { return m_useTime; } void UseATR(bool value) { m_useATR = value; } bool UseATR(void) const { return m_useATR; } void UseMA(bool value) { m_useMA = value; } bool UseMA(void) const { return m_useMA; } void UseSwingContext(bool value) { m_useSwingContext = value; } bool UseSwingContext(void) const { return m_useSwingContext; } void UseNews(bool value) { m_useNews = value; } bool UseNews(void) const { return m_useNews; } void NewsFeatureWindowMinutes(int value) { m_newsFeatureWindowMinutes = value; } int NewsFeatureWindowMinutes(void) const { return m_newsFeatureWindowMinutes; } void UseCrossAsset(bool value) { m_useCrossAsset = value; } bool UseCrossAsset(void) const { return m_useCrossAsset; } void UseSpreadFeature(bool value) { m_useSpreadFeature = value; } bool UseSpreadFeature(void) const { return m_useSpreadFeature; } void AutoTuneIndicators(bool value) { m_autoTuneIndicators = value; } void UseAltData(bool value) { m_altDataEnabled = value; } //--- TOPOLOGY VIEW published read/write API - see Expert\Topology\ITopologyView.mqh for the //--- contract these serve. This one is the DERIVED alt-data flag InitFeatureIndicators sets once //--- the feature width is known - distinct from UseAltData(bool) above, which is the operator's //--- opt-in switch (m_altDataEnabled). bool TopologyUseAltData(void) const { return m_useAltData; } //--- control-panel API (Warrior_EA.mq5): current-config-only training/weights control. //--- "current config" == this signal instance's own m_fileName (symbol+period+id+topology), //--- never touches another signal type's or another symbol/timeframe's saved files. void PauseTraining(void) { m_trainingPaused = true; PrintVerbose(ID + ": training paused by user (era " + IntegerToString(m_eraCount) + ")"); } void ResumeTraining(void) { m_trainingPaused = false; PrintVerbose(ID + ": training resumed by user (era " + IntegerToString(m_eraCount) + ")"); } bool IsTrainingPaused(void) const { return m_trainingPaused; } bool IsTrainingStopped(void) const { return m_trainingStopRequested; } bool TrainingComplete(void) const { return m_trainingComplete; } //--- Set by OnDeinit before it calls StopTraining(), so FinalizeTrainRun() can tell a user-pressed Stop //--- (persist the deployed model now - nothing else will) from a shutdown (PersistWeightsOnShutdown is //--- moments away and writes the same bytes). See the guard in FinalizeTrainRun. void MarkShutdown(void) { m_shutdownInProgress = true; } //--- THE ONE QUESTION every long loop in this class must ask: has this program been asked to //--- stop? These two are terminal - once either is true the program is going away. bool ShutdownRequested(void) const { return (IsStopped() || m_shutdownInProgress); } //+------------------------------------------------------------------+ //| PUBLISHED READ API for training-side collaborators. | //| | //| Everything a baseline, a geometry scan or a redundancy report | //| needs to see, and nothing else. Before this, such code lived | //| inside the class purely so it could reach these caches - which is | //| why a 951-line diagnostic could not be moved, replaced or tested | //| on its own. Collaborators reach these through CTrainingDataView | //| and never name this class. | //| | //| Each row accessor OWNS ITS BOUNDS TEST and answers false for a | //| bar it has nothing for. That is deliberate: the callers used to | //| carry their own ArraySize() guards, and a caller that forgot one | //| read past the end of a cache that is shorter than the bar count | //| for the whole warm-up. | //+------------------------------------------------------------------+ int DataHistoryBars(void) const { return (int)m_historyBars; } int DataFeaturesPerBar(void) const { return m_neuronsCount; } int DataLabelResolutionBars(void) const { return LabelResolutionBars(); } int DataPurgeBars(void) const { return CalibPurgeBars(); } int DataCalibrationHiIndex(const int totalIter, const int oosCutoff) { return CalibHiIndex(totalIter, oosCutoff); } bool DataHasLabel(const int bar) const { return (bar >= 0 && bar < ArraySize(m_labelCacheHasValue) && m_labelCacheHasValue[bar]); } bool DataIsBuyLabel(const int bar) const { return (DataHasLabel(bar) && bar < ArraySize(m_labelCacheBuy) && m_labelCacheBuy[bar]); } bool DataIsSellLabel(const int bar) const { return (DataHasLabel(bar) && bar < ArraySize(m_labelCacheSell) && m_labelCacheSell[bar]); } //--- Bars-to-resolution of the bar's cached label; 0 for an unresolved bar. int DataLabelResolveAge(const int bar) const { return (DataHasLabel(bar) && bar < ArraySize(m_labelResolveAge)) ? m_labelResolveAge[bar] : 0; } //--- -1 means "this bar calls no pivot" - either Neutral, or not resolved. Never 0-as-unknown: //--- 0 is a REAL value here (the pivot is on the very next bar) and the two must not collide. int LabelBarsToPivot(const int bar) const { return (DataHasLabel(bar) && bar < ArraySize(m_labelBarsToPivot)) ? m_labelBarsToPivot[bar] : -1; } //--- -2.0 is the "never scored" SENTINEL, not a small confidence - see m_arrowSignalCache. //--- The threshold that turns a raw value into a side depends on the head's output width, which //--- is why this conversion belongs here and not in whatever is reading. A bar the model called //--- Neutral answers false, exactly like a bar it never scored: neither is a directional call. bool DataDirectionalCall(const int bar, bool &isBuy, double &magnitude) { isBuy = false; magnitude = 0.0; if(bar < 0 || bar >= ArraySize(m_arrowSignalCache)) return false; double v = m_arrowSignalCache[bar]; if(v == -2.0 || !MathIsValidNumber(v)) return false; ENUM_SIGNAL side = DoubleToSignal(v); if(side != Buy && side != Sell) return false; isBuy = (side == Buy); magnitude = MathAbs(v); return true; } string DataId(void) const { return ID; } //--- The shared scratch buffer, for the views that hand it to a collaborator. CArrayDouble *DataTempData(void) { return TempData; } ENUM_ACTIVATION DataHiddenLayerActivation(void) { return HiddenLayerActivation(); } //--- The short id (m_id, set by SetIdentity) - CConfigLock's WarriorAI__ global-variable //--- name is the only outside reader; no prior public accessor exposed it. string ConfigLockShortId(void) const { return m_id; } bool DataIsEnsembleMember(void) const { return m_ensembleMember; } int DataEnsembleIndex(void) const { return m_ensembleIndex; } //--- calls == 0 means the gate has NOT scored yet, which is not a gate that scored zero - so //--- this answers false there rather than handing back a 0% that reads as a measurement. bool DataGateReference(double &precPct, int &calls, double &chancePct) const { precPct = m_bestDirPrecPct; calls = m_bestDirCalls; chancePct = m_bestChancePrecPct; return (calls > 0); } double DataEffectiveSampleSize(const double rawN) const { return EffectiveSampleSize(rawN); } //--- THE HANDLE COLLABORATORS ARE GIVEN. They take a CTrainingDataView* and so cannot reach //--- anything above that is not on it - which is the point of handing them this and not `this`. CTrainingDataView *TrainingData(void) { return GetPointer(m_trainingData); } //--- Same doctrine, chart side: CChartUI takes a CChartView* and never `this`. CChartView *ChartView(void) { return GetPointer(m_chartView); } //--- Same doctrine, persistence side: CModelPersistence takes a CPersistenceView* and never `this`. CPersistenceView *PersistenceView(void) { return GetPointer(m_persistenceView); } //--- Same doctrine, online-learning side: COnlineLearning takes a COnlineLearningView* and never `this`. COnlineLearningView *OnlineLearningView(void) { return GetPointer(m_onlineLearningView); } //--- Same doctrine, topology side: CTopology takes a CTopologyView* and never `this`. CTopologyView *TopologyView(void) { return GetPointer(m_topologyView); } //--- Same doctrine, feature-building side: CFeatureBuilder takes a CFeaturesView* and never `this`. CFeaturesView *FeaturesView(void) { return GetPointer(m_featuresView); } //--- Same doctrine, config-lock side: CConfigLock takes a CConfigLockView* and never `this`. CConfigLockView *ConfigLockView(void) { return GetPointer(m_configLockView); } //--- CHART VIEW published read API - see Expert\Chart\IChartView.mqh for the contract these //--- serve. Same doctrine as the Data*() block above: named and bounds-checked where the raw //--- member would let a caller run past a cache, so CChartUI never pokes a member directly. string ChartFileName(void) const { return m_fileName; } int ChartDigits(void) const { return m_symbol.Digits(); } string ChartSymbolName(void) const { return m_symbol.Name(); } //--- NOT "ChartPeriod" - MQL5's builtin global ChartPeriod(chart_id) exists, and a 0-arg member //--- of the same name hides it for every unqualified caller in this class's own body (Lifecycle.mqh's //--- ChartPeriod(owner) resolved here instead, "wrong parameters count, 1 passed but 0 requires"). ENUM_TIMEFRAMES ChartTimeframe(void) const { return (ENUM_TIMEFRAMES)m_period; } bool ChartModelLoadedFromDisk(void) const { return m_modelLoadedFromDisk; } bool ChartTrainingComplete(void) const { return m_trainingComplete; } int ChartOutputNeuronsCount(void) const { return m_outputNeuronsCount; } bool ChartNetReady(void) const { return (CheckPointer(Net) != POINTER_INVALID && m_isInitialized); } datetime ChartBarTime(const int idx) { return m_Time.GetData(idx); } double ChartBarClose(const int idx) { return m_Close.GetData(idx); } int ChartAvailableBars(void) { return Bars(m_symbol.Name(), PERIOD_CURRENT); } //--- ONE bar through the deployed (shadow-preferred) net for AdvanceChartSignalRescan: builds the //--- feature window, forwards, and returns both the raw argmax-basis signal (pre logit-prior //--- correction, for the raw tally) and the adjusted one (for the cache). False = the window //--- could not be built (bar skipped, not scored) - same as the inline body this replaces. bool ChartScoreBarForRescan(const int idx, double &rawSignal, double &adjustedSignal) { rawSignal = 0.0; adjustedSignal = -2.0; if(!BuildFeatureWindow(idx)) return false; CNet *deployNet = m_onlineLearning.DeployNet(); deployNet.feedForward(TempData); deployNet.getResults(TempData); if(m_outputNeuronsCount == 1) { adjustedSignal = TempData[0]; rawSignal = TempData[0]; } else { rawSignal = ApplyClassificationSoftmax(); adjustedSignal = AdjustedSignalFromSoftmax(); } return true; } //--- PREDICTION CACHE (m_arrowSignalCache). Stays signal-owned - Training.mqh writes it directly //--- every era and DataDirectionalCall already reads it for the baseline comparator - so this is //--- a bounds-checked window onto shared state, not a copy. int ChartPredictionCacheSize(void) const { return ArraySize(m_arrowSignalCache); } double ChartPredictionAt(const int idx) const { return (idx >= 0 && idx < ArraySize(m_arrowSignalCache)) ? m_arrowSignalCache[idx] : -2.0; } void ChartSetPredictionAt(const int idx, const double value) { if(idx >= 0 && idx < ArraySize(m_arrowSignalCache)) m_arrowSignalCache[idx] = value; } void ChartResizePredictionCache(const int size, const double fillValue) { ArrayResize(m_arrowSignalCache, size); ArrayInitialize(m_arrowSignalCache, fillValue); } //--- THE OVERLAY'S DATA SOURCE, PUBLISHED. One implementation, two callers: the era end //--- (RankTiersFromOos, at pass-3 completion) and a completed chart rescan. See its definition for //--- why the second caller is what makes a DEPLOYED model's arrows rebuildable at all. void PublishOverlaySnapshotFromCache(void); //--- THE REPLAY PASS. Scores the rescan's per-bar signals against the label cache and hands the //--- result to RankTiersFromOos(), so a converged model can mint its tier ladder without a //--- training run. See the definition. void ScoreReplayFromCache(void); //--- Drives the deployed-model rebuild (labels -> rescan -> score -> rank -> save) one timer slice //--- at a time. Returns true while it still owns the slice. bool AdvanceDeployedRebuild(void); //--- STAGE 3, and the completion hook for every rescan. A manual rescan (the panel's Show Signals) //--- only needs the snapshot republished; a rebuild rescan additionally has to SCORE what it just //--- computed, rank the ladder from it, and write the result down so the next restart inherits it. //--- Both arrive here so there is one place that knows what a finished rescan means. void OnChartRescanComplete(void) { if(m_deployedRebuildStage == 1) { m_deployedRebuildStage = 2; //--- Ranks the ladder, which publishes the overlay snapshot and arms the sweep on its way //--- through - see ScoreReplayFromCache(). ScoreReplayFromCache(); //--- PERSIST IMMEDIATELY. The whole failure this repairs is state that existed in memory and //--- was never written down; recomputing it and then not saving it would repeat that exactly, //--- and the next restart would pay for the replay all over again. if(m_tiersSelfRanked && !SaveModelStats(m_activeFileName, m_activeFileCommon)) Print(ID + ": ERROR - rebuilt the tier ladder but could not persist it to .stats;" " it will have to be replayed again on the next attach."); return; } PublishOverlaySnapshotFromCache(); } long ChartEraCount(void) const { return m_eraCount; } long ChartCumIsTotal(void) const { return m_cumIsTotal; } long ChartCumIsCorrect(void) const { return m_cumIsCorrect; } long ChartCumOosTotal(void) const { return m_cumOosTotal; } long ChartCumOosCorrect(void) const { return m_cumOosCorrect; } void ChartOosTally(int &buyPredicted, int &sellPredicted, int &buyPredictedHits, int &sellPredictedHits) const { buyPredicted = m_oos.buyPredicted; sellPredicted = m_oos.sellPredicted; buyPredictedHits = m_oos.buyPredictedHits; sellPredictedHits = m_oos.sellPredictedHits; } string ChartPassLabel(void) const { return m_passLabel; } int ChartPassProgressPct(void) const { return m_passProgressPct; } int ChartOosSplitPct(void) const { return m_oosSplitPct; } int ChartOosSamples(void) const { return m_oosSamples; } void ChartClassCounts(int &predBuy, int &predSell, int &predNeutral, int &trueBuy, int &trueSell, int &trueNeutral) const { predBuy = m_countBuySignals; predSell = m_countSellSignals; predNeutral = m_countNeutralSignals; trueBuy = m_trueBuyCount; trueSell = m_trueSellCount; trueNeutral = m_trueNeutralCount; } void ChartOosRecallPct(int &buyRecallPct, int &sellRecallPct) const { buyRecallPct = m_lastBuyRecallPct; sellRecallPct = m_lastSellRecallPct; } void ChartOosLivePrecision(int &buyPrecPct, int &buyFired, int &sellPrecPct, int &sellFired) const { buyPrecPct = m_lastBuyFiredPrecPct; buyFired = m_lastBuyFired; sellPrecPct = m_lastSellFiredPrecPct; sellFired = m_lastSellFired; } double ChartForecast(void) const { return dForecast; } double ChartErrorPct(void) const { return dError; } double ChartOosForecast(void) const { return dOosForecast; } double ChartOosErrorPct(void) const { return dOosError; } double ChartNetRecentAverageError(void) const { return (CheckPointer(Net) != POINTER_INVALID) ? Net.getRecentAverageError() : 0.0; } //--- Forwards to PROTECTED members the adapter cannot reach directly - CAIBaseChartView is not a //--- derived class (MQL5 has no `friend`), so every protected call the view needs is re-published //--- here, same doctrine as the rest of this block. string ChartArrowPrefix(void) const { return ArrowPrefix(); } string ChartDisplayName(void) const { return DisplayName(); } bool ChartBothDirectionsTradeable(void) const { return BothDirectionsTradeable(); } bool ChartResizeBuffers(const int barIndex) { return ResizeBuffers(barIndex); } bool ChartRefreshData(void) { return RefreshData(); } int ChartServableBars(const int want, const string context) { return ServableBars(want, context); } void ChartEnsureShadowNet(void) { EnsureShadowNet(); } ENUM_SIGNAL ChartDoubleToSignal(const double value) { return DoubleToSignal(value); } bool ChartDisplayInference(void) { return DisplayInference(); } //--- PERSISTENCE VIEW published read/write API - see Expert\Persistence\IPersistenceView.mqh for //--- the contract these serve. Read+write, unlike the Chart*() block above: Persistence's whole //--- job is loading saved state BACK into the signal. bool PersistNetLoaded(void) const { return CheckPointer(Net) != POINTER_INVALID; } bool PersistUsesConvStage(void) const { return UsesConvStage(); } uint PersistNetFirstConvWindow(void) const { return (CheckPointer(Net) != POINTER_INVALID) ? Net.FirstConvWindow() : 0; } int PersistConvReceptiveFieldBars(void) const { return ConvReceptiveFieldBars(); } ENUM_ACTIVATION PersistOutputLayerActivation(void) const { return OutputLayerActivation(); } bool PersistNetEnforceOutputActivation(const ENUM_ACTIVATION intended, ENUM_ACTIVATION &stale) { return (CheckPointer(Net) != POINTER_INVALID) ? Net.EnforceOutputActivation(intended, stale) : false; } void PersistSetTopologySuperseded(const bool v) { m_topologySuperseded = v; } //--- SaveModelStats()/LoadModelStats() fields - one scalar getter+setter per on-disk field double PersistPriorBuy(void) const { return m_priorBuy; } void PersistSetPriorBuy(const double v) { m_priorBuy = v; } double PersistPriorSell(void) const { return m_priorSell; } void PersistSetPriorSell(const double v) { m_priorSell = v; } double PersistPriorNeutral(void) const { return m_priorNeutral; } void PersistSetPriorNeutral(const double v) { m_priorNeutral = v; } double PersistConfidenceCalScale(void) const { return m_confidenceCalScale; } void PersistSetConfidenceCalScale(const double v) { m_confidenceCalScale = v; } bool PersistMqlInferenceValidated(void) const { return m_mqlInferenceValidated; } void PersistSetMqlInferenceValidated(const bool v) { m_mqlInferenceValidated = v; } datetime PersistOnlineLearnedUpToTime(void) const { return m_onlineLearning.LearnedUpToTime(); } void PersistSetOnlineLearnedUpToTime(const datetime v) { m_onlineLearning.SetLearnedUpToTime(v); } double PersistOnlineRollingAcc(void) const { return m_onlineLearning.RollingAcc(); } void PersistSetOnlineRollingAcc(const double v) { m_onlineLearning.SetRollingAcc(v); } long PersistOnlineSamples(void) const { return m_onlineLearning.Samples(); } void PersistSetOnlineSamples(const long v) { m_onlineLearning.SetSamples(v); } int PersistLastBuyFiredPrecPct(void) const { return m_lastBuyFiredPrecPct; } void PersistSetLastBuyFiredPrecPct(const int v) { m_lastBuyFiredPrecPct = v; } int PersistLastSellFiredPrecPct(void) const { return m_lastSellFiredPrecPct; } void PersistSetLastSellFiredPrecPct(const int v) { m_lastSellFiredPrecPct = v; } int PersistLastBuyRecallPct(void) const { return m_lastBuyRecallPct; } void PersistSetLastBuyRecallPct(const int v) { m_lastBuyRecallPct = v; } int PersistLastSellRecallPct(void) const { return m_lastSellRecallPct; } void PersistSetLastSellRecallPct(const int v) { m_lastSellRecallPct = v; } int PersistLastBuyFired(void) const { return m_lastBuyFired; } void PersistSetLastBuyFired(const int v) { m_lastBuyFired = v; } int PersistLastSellFired(void) const { return m_lastSellFired; } void PersistSetLastSellFired(const int v) { m_lastSellFired = v; } void PersistSetCumIsCorrect(const long v) { m_cumIsCorrect = v; } void PersistSetCumIsTotal(const long v) { m_cumIsTotal = v; } void PersistSetCumOosCorrect(const long v) { m_cumOosCorrect = v; } void PersistSetCumOosTotal(const long v) { m_cumOosTotal = v; } //--- THE TIER LADDER, for .stats (WST7). See LiveVoteContribution(): until RankTiersFromOos() has //--- run once, m_pattern_0..3 hold the constructor's stock 25/50/75/100 - the WRONG UNIT, not a //--- weak opinion - and the member is deliberately silenced. That silencing is correct while //--- training and catastrophic on a resume: a converged model runs no further passes, so without //--- these fields it can never speak again. int PersistTierWeight(const int tier) const { switch(tier) { case 0: return m_pattern_0; case 1: return m_pattern_1; case 2: return m_pattern_2; default: return m_pattern_3; } } void PersistSetTierWeight(const int tier, const int v) { switch(tier) { case 0: m_pattern_0 = v; break; case 1: m_pattern_1 = v; break; case 2: m_pattern_2 = v; break; default: m_pattern_3 = v; break; } } void PersistSetModuleTrustWeight(const double v) { Weight(v); } void PersistSetTiersSelfRanked(const bool v) { m_tiersSelfRanked = v; } //--- WST8: the pair HasDemonstratedEdge() falls back to. Deliberately NOT the era pair - that one //--- belongs to an era that will not exist after a restart, and writing it down would claim a //--- measurement for weights that may have moved since. double PersistCertifiedPrecPct(void) const { return m_certifiedPrecPct; } double PersistCertifiedChancePct(void) const { return m_certifiedChancePct; } void PersistSetCertifiedEdge(const double precPct, const double chancePct) { if(precPct < 0.0 || chancePct < 0.0) return; m_certifiedPrecPct = precPct; m_certifiedChancePct = chancePct; } //--- ValidateCpuInference() - the whole Net-pointer/throwaway-clone core, consolidated: this is //--- irreducible pointer/object work, not signal state, same doctrine as ChartScoreBarForRescan. bool PersistRunCpuInferenceSelfCheck(double &maxDiff) { maxDiff = DBL_MAX; if(CheckPointer(Net) == POINTER_INVALID || CheckPointer(TempData) == POINTER_INVALID) return false; if(!BuildFeatureWindow(0)) return false; Net.SetBatchNormFrozen(true); bool refOk = Net.feedForward(TempData); Net.SetBatchNormFrozen(false); if(!refOk) return false; CArrayDouble *refOut = new CArrayDouble(); if(CheckPointer(refOut) == POINTER_INVALID) return false; Net.getResults(refOut); CNet *cpu = new CNet(NULL); if(CheckPointer(cpu) == POINTER_INVALID) { delete refOut; return false; } cpu.SetCpuInference(true); double e, u, f; datetime tm; long era; bool complete; double ip[]; bool loaded = cpu.Load(m_activeFileName + ".nnw", e, u, f, tm, m_activeFileCommon, era, complete, ip); if(loaded) cpu.SetBatchNormFrozen(true); bool pass = false; if(loaded && cpu.feedForward(TempData)) { CArrayDouble *cpuOut = new CArrayDouble(); if(CheckPointer(cpuOut) != POINTER_INVALID) { cpu.getResults(cpuOut); if(cpuOut.Total() == refOut.Total() && refOut.Total() > 0) { maxDiff = 0.0; for(int i = 0; i < refOut.Total(); i++) maxDiff = MathMax(maxDiff, MathAbs(refOut.At(i) - cpuOut.At(i))); pass = (maxDiff <= CPU_INFERENCE_MAX_DIFF); } delete cpuOut; } } delete cpu; delete refOut; return pass; } //--- SaveTopologyConfiguration()/LoadAndCompareTopologyConfiguration() fields beyond the params //--- every caller already passes void PersistSetDirConfThreshold(const double v) { m_dirConfThreshold = v; } double PersistBestDirConfThreshold(void) const { return m_bestDirConfThreshold; } void PersistSetBestDirConfThreshold(const double v) { m_bestDirConfThreshold = v; } string PersistCrossAssetPairsPinned(void) const { return m_crossAssetPairsPinned; } void PersistSetCrossAssetPairsPinned(const string v) { m_crossAssetPairsPinned = v; } bool PersistCrossAssetCfgSaved(void) const { return m_crossAssetCfgSaved; } void PersistSetCrossAssetCfgSaved(const bool v) { m_crossAssetCfgSaved = v; } string PersistAltDataNamesPinned(void) const { return m_altDataNamesPinned; } void PersistSetAltDataNamesPinned(const string v) { m_altDataNamesPinned = v; } void PersistApplyAltDataPinnedNames(const string v) { m_altData.SetPinnedNames(v); } //--- LoadNetWithRetry() bool PersistLoadNetOnce(double &indicatorParams[]) { return Net.Load(m_activeFileName + ".nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, indicatorParams); } //--- ONLINE-LEARNING VIEW published read/write API - see Expert\OnlineLearning\IOnlineLearningView.mqh //--- for the contract these serve. New wrappers for protected members/methods COnlineLearning has //--- no other way to reach (MQL5 has no `friend`); everything already public elsewhere (DataId(), //--- ChartEraCount(), PersistPriorBuy() etc.) is reused directly by the adapter instead of repeated here. string OnlineActiveFileName(void) const { return m_activeFileName; } bool OnlineActiveFileCommon(void) const { return m_activeFileCommon; } double OnlineDUndefine(void) const { return dUndefine; } datetime OnlineDtStudied(void) const { return dtStudied; } CNet *OnlineNet(void) { return Net; } bool OnlineBuildFeatureWindow(const int r) { return BuildFeatureWindow(r); } double OnlineApplyClassificationSoftmax(void) { return ApplyClassificationSoftmax(); } double OnlineAdjustedSignalFromSoftmax(void) { return AdjustedSignalFromSoftmax(); } //--- The label the online step learns from: resolve-on-demand through the SAME finality-gated //--- cache path training uses, then read the cache. Undefine = still unresolved, do not learn. ENUM_SIGNAL OnlineBarLabel(const int idx) { if(idx >= 0 && idx < ArraySize(m_labelCacheHasValue) && !m_labelCacheHasValue[idx]) AdvanceSwingLabelState(idx, ArraySize(m_labelCacheHasValue)); if(idx < 0 || idx >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[idx]) return Undefine; return m_labelCacheBuy[idx] ? Buy : (m_labelCacheSell[idx] ? Sell : Neutral); } int OnlineCalibLoIndex(const int oosCutoff) { return CalibLoIndex(oosCutoff); } int OnlineCalibBandBars(const int totalIter, const int oosCutoff) { return CalibBandBars(totalIter, oosCutoff); } bool OnlineInferenceOnlyFlag(void) const { return m_inferenceOnly; } bool OnlineTrainRunActiveFlag(void) const { return m_trainRunActive; } bool OnlineEnableLearningFlag(void) const { return m_onlineLearning.Enabled(); } bool OnlineEnsureBarCachesCapacity(const int bars) { return EnsureBarCachesCapacity(bars); } double OnlineAtrMain(const int idx) { return m_ATR.Main(idx); } double OnlineSpreadPrice(void) { return (double)m_symbol.Spread() * m_symbol.Point(); } int OnlineConfidenceTier(void) { return ConfidenceTier(); } double OnlinePatternWeightForTier(const int tier) { return PatternWeightForTier(tier); } string OnlinePatternTableName(const string filterID, const string pattern, const string direction) { return PatternTableName(filterID, pattern, direction); } void OnlineRegisterSignal(int year, int month, int day, int DOW, int hour, int minutes, string tableName, string pattern, string direction, double entryPrice, double exitPrice, string result, double netVote) { RegisterSignal(year, month, day, DOW, hour, minutes, tableName, pattern, direction, entryPrice, exitPrice, result, netVote); } double OnlinePrevSignal(void) const { return dPrevSignal; } void OnlineSetPrevSignal(const double v) { dPrevSignal = v; } bool OnlineSaveModelStats(void) { return SaveModelStats(m_activeFileName, m_activeFileCommon); } void OnlineFlattenIndicatorParams(double &ip[]) { m_indicatorTuner.Flatten(ip); } double OnlineModelEta(void) const { return m_modelEta; } //--- TOPOLOGY VIEW published read/write API (continued) - see Expert\Topology\ITopologyView.mqh. //--- New wrappers for protected members/methods CTopology has no other way to reach (MQL5 has no //--- `friend`); everything already public elsewhere (DataId(), ChartOutputNeuronsCount(), //--- PersistUsesConvStage() etc.) is reused directly by the adapter instead of repeated here. int TopologyOptimizationAlgo(void) const { return m_optimizationAlgo; } int TopologyMinTrainYear(void) const { return m_minTrainYear; } int TopologyFractalPeriods(void) const { return m_fractalPeriods; } int TopologyConvFilterCount(void) const { return m_convFilterCount; } int TopologyLstmHiddenSize(void) const { return m_lstmHiddenSize; } int TopologyInitialNeuronsCount(void) const { return m_initialNeuronsCount; } int TopologyHiddenLayersCount(void) const { return m_hiddenLayersCount; } bool TopologyUsesLstmStage(void) const { return UsesLstmStage(); } bool TopologyHasConvBeforeLstm(void) const { return HasConvBeforeLstm(); } int TopologyNetInputWidth(void) const { return NetInputWidth(); } bool TopologyAddCustomLayers(CArrayObj *topology) { return AddCustomLayers(topology); } //--- The LABEL'S pivot source. Handed over as a HANDLE, not as the CiCustom: CTopology reads it at //--- init via CopyBuffer, before ResizeBuffers() has sized the wrapper's own buffers. int TopologyZigZagHandle(void) { return m_zigZag.Handle(); } //--- The whole Net-pointer swap, consolidated: not signal state, irreducible pointer/object work - //--- same doctrine as PersistRunCpuInferenceSelfCheck/ChartScoreBarForRescan. bool TopologyReplaceNetFromTopology(CArrayObj *topology) { if(CheckPointer(Net) != POINTER_INVALID) delete Net; Net = new CNet(topology); return (CheckPointer(Net) != POINTER_INVALID); } void TopologyResetOnlineLearningForFreshTopology(void) { m_onlineLearning.ResetForFreshTopology(); } //--- CAPACITY SIZING WAS POOL-BLIND: EstimatedInSampleBars() budgeted the first hidden layer from //--- THIS CHART's own bars only, while Use_Training_Pool feeds the trainer up to TRAINPOOL_MAX_ROWS //--- peer rows (Training\TrainingPool.mqh) it never counted - every model was sized several times //--- narrower than its actual training set, and the "cannot support N features" warning //--- correspondingly overstated. header-only census (TrainPoolEstimateAvailableRows), never opens //--- a row. //--- //--- TWO CONSERVATIVE DISCOUNTS, deliberately not a 1:1 row count: //--- 1. Divided by this chart's own SwingLifespanEstimate() - a peer row is not more independent //--- of its OWN neighbours than this chart's bars are of theirs, and the label-overlap //--- deflation this chart already applies to its own bars is the only overlap estimate //--- available (peers do not publish their own). //--- 2. Divided by the number of CONTRIBUTING peer files - pooled instruments are cross- //--- sectionally correlated (EURUSD/USDCAD/USDJPY especially, all USD-legged), so N peers do //--- not carry N independent peers' worth of evidence. Treating the whole pool as worth //--- roughly one peer's independent contribution is a floor, not a measurement - there is no //--- cross-instrument correlation structure measured anywhere in this codebase to do better. //--- Only ever narrows the pool's contribution, never invents capacity beyond what //--- TrainPoolEstimateAvailableRows() actually found on disk. //--- DIAGNOSTIC, print-once: this number reached production 2026-08-25 reporting EXACTLY the //--- pre-fix (0-contribution) figure on every chart's first log, which is ambiguous between //--- "correctly found no compatible peer data yet" and "a bug in this function" from the log //--- alone - see [[project_audit_20260825_mega_patch]]. Remove once a run confirms a nonzero //--- census under known-good pool files. bool m_poolCensusLogged; double TopologyPooledIndependentBars(void) { if(!Use_Training_Pool) return 0.0; //--- NetInputWidth(), NOT DataFeaturesPerBar(): that is what CTrainPoolWriter/Reader actually //--- key the pool's width field on (see Training.mqh's own m_trainPoolReader.Adopt() call) - //--- the per-bar feature count alone would under-specify the row layout for any conv/LSTM //--- front-end, where the vector reaching the dense stack differs from the raw feature count. string fp = m_topology.BuildModelFingerprint(); int peerFiles = 0; int rows = TrainPoolEstimateAvailableRows(fp, ChartSymbolName(), (int)ChartTimeframe(), NetInputWidth(), peerFiles); double lifespan = m_topology.SwingLifespanEstimate(); double result = (rows <= 0 || peerFiles <= 0) ? 0.0 : (double)rows / MathMax(1.0, lifespan) / (double)peerFiles; if(!m_poolCensusLogged) { m_poolCensusLogged = true; PrintFormat("%s: POOL CENSUS - fingerprint=%s width=%d symbol=%s period=%d -> %d compatible" " row(s) from %d peer file(s), lifespan=%.1f -> +%.1f independent observations" " added to this model's capacity budget.", DataId(), fp, NetInputWidth(), ChartSymbolName(), (int)ChartTimeframe(), rows, peerFiles, lifespan, result); } return result; } //--- FEATURES VIEW published read/write API - see Expert\Features\IFeaturesView.mqh for the //--- contract these serve. New wrappers for protected members/methods CFeatureBuilder has no other //--- way to reach (MQL5 has no `friend`); everything already public elsewhere (DataId(), //--- ChartSymbolName(), UseMA() etc.) is reused directly by the adapter instead of repeated here. //--- m_Open/m_Close/m_High/m_Low/m_Time/m_ATR/m_zigZag stay signal-owned (real use in Labels.mqh/ //--- AutoTune.mqh/Training.mqh too) - these are READ-ONLY windows onto them, never the Init/Resize/ //--- Refresh lifecycle (that stays in Expert\AIBase\Features.mqh, see InitOpen's comment). double FeatureOpenAt(const int idx) const { return m_Open.GetData(idx); } double FeatureHighAt(const int idx) const { return m_High.GetData(idx); } double FeatureLowAt(const int idx) const { return m_Low.GetData(idx); } int FeatureAtrBarsCalculated(void) const { return m_ATR.BarsCalculated(); } int FeatureAtrHandle(void) const { return m_ATR.Handle(); } int FeatureZigZagBarsCalculated(void) const { return m_zigZag.BarsCalculated(); } int FeatureZigZagHandle(void) const { return m_zigZag.Handle(); } double FeatureSymbolPoint(void) const { return m_symbol.Point(); } CIndicators *FeatureIndicatorsPtr(void) const { return m_indicatorsPtr; } //--- Whole-object pointer, same doctrine as TrainingData()/ChartView() etc: CADIndicatorTuner //--- already declares its fields public on ITS OWN class, so CFeatureBuilder reads //--- .maPeriod/.ichiKijun/.adWES.lookback/etc and calls .Flatten()/.Unflatten() straight through //--- this pointer instead of one accessor per nested field. CADIndicatorTuner *FeatureIndicatorTuner(void) { return GetPointer(m_indicatorTuner); } //--- Same doctrine for the cross-asset panel - CCrossAssetPanel's build/query API is already //--- public on its own class. CCrossAssetPanel *FeatureCrossAsset(void) { return GetPointer(m_crossAsset); } int FeatureAltDataFeatureCount(void) const { return m_altData.FeatureCount(); } void FeatureAltDataEnsureFresh(const datetime asOf) { m_altData.EnsureFresh(asOf); } void FeatureAltDataFeatures(const datetime t, double &out[]) { m_altData.Features(t, out); } //--- The SaveTopologyConfiguration() call BuildCrossAssetPanel makes when it first pins the //--- cross-asset pair set to the .cfg - consolidated into one call, same doctrine as //--- TopologyReplaceNetFromTopology (irreducible persistence side-effect, not signal state). bool FeatureCommitCrossAssetPin(void) { return SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount, m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo, m_historyBars, m_outputNeuronsCount, m_neuronsCount, LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, m_isInitialized, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount, m_lstmHiddenSize, m_activeFileCommon); } //--- Zero-skill precision reference for the detectability report: the prebuild-measured base //--- rate of the LARGER directional class. -1 until the prebuild has tallied. double FeatureChanceRatePct(void) const { long tot = (long)m_labelPrebuildBuyCount + m_labelPrebuildSellCount + m_labelPrebuildNeutralCount; if(tot <= 0) return -1.0; return 100.0 * (double)MathMax(m_labelPrebuildBuyCount, m_labelPrebuildSellCount) / tot; } int FeatureFirstLayerFanIn(void) const { return FirstLayerFanIn(); } //--- The lifespan the TOPOLOGY was sized against, so the CAPACITY line can print it beside the one //--- the labels actually measured. Those two agreeing is what makes the derived shape trustworthy. double FeatureSwingLifespanEstimate(void) const { return m_topology.SwingLifespanEstimate(); } double FeatureMeanLabelLifespan(void) const { return MeanLabelLifespan(); } double FeatureEstimatedInSampleBarsRaw(void) const { return EstimatedInSampleBarsRaw(); } bool FeatureFindConfirmedZigZagPivot(const int fromIdx, int &pivotIdx, double &pivotPrice, bool &pivotIsLow) { return FindConfirmedZigZagPivot(fromIdx, pivotIdx, pivotPrice, pivotIsLow); } //--- FEATURE-ROW CACHE (m_featureCache/m_featureCacheHasValue/m_featureCacheValid) - stays //--- signal-owned (Labels.mqh ArrayResize()s it, Training.mqh ArrayInitialize()s it on a param //--- change), reached element-by-element the same way BufferTempData() always indexed it. int FeatureCacheSize(void) const { return ArraySize(m_featureCacheHasValue); } bool FeatureCacheHasValue(const int idx) const { return m_featureCacheHasValue[idx]; } bool FeatureCacheIsValid(const int idx) const { return m_featureCacheValid[idx]; } double FeatureCacheAt(const int flatIdx) const { return m_featureCache[flatIdx]; } //--- Bulk cache-hit read - see IFeaturesView.mqh's declaration. One ArrayCopy against the same //--- backing array FeatureCacheAt() indexes one element at a time. void FeatureCacheBlock(const int base, const int count, double &out[]) const { ArrayCopy(out, m_featureCache, 0, base, count); } void FeatureCacheSetAt(const int flatIdx, const double v) { m_featureCache[flatIdx] = v; } //--- Bulk counterpart to FeatureCacheBlock() above, for the write side. void FeatureCacheSetBlock(const int base, const int count, const double &values[]) { ArrayCopy(m_featureCache, values, base, 0, MathMin(count, ArraySize(values))); } //--- Always set together at store time - see BufferTempData's original "ONLY SUCCESSES ARE //--- CACHED" block. void FeatureCacheMarkStored(const int idx) { m_featureCacheHasValue[idx] = true; m_featureCacheValid[idx] = true; } //--- Params just changed (ReInitTunableIndicators) or a dead handle was just recreated //--- (RepairDeadIndicatorHandles) - every cached row was computed against the OLD handle. void FeatureCacheInvalidateAll(void) { ArrayInitialize(m_featureCacheHasValue, false); } //--- SwingPivotDirectionLabel() (Labels.mqh) reads m_Close/m_ATR/m_zigZag directly - a repair //--- that recreates any of those handles stales every label already resolved against the old //--- one, the same way FeatureCacheInvalidateAll() stales the feature cache. Every reader gates //--- on m_labelCacheHasValue (DataHasLabel()), so clearing it alone forces a full relabel. void LabelCacheInvalidateAll(void) { ArrayInitialize(m_labelCacheHasValue, false); } //--- m_featureFailBlock/m_featureFailIdx/m_windowFailSlot/m_windowFailTotal stay signal-owned - //--- Training.mqh reads all four directly for the pass-1 stall report. void FeatureSetFailBlock(const string block) { m_featureFailBlock = block; } void FeatureSetFailIdx(const int idx) { m_featureFailIdx = idx; } void FeatureSetWindowFail(const int slot, const int total) { m_windowFailSlot = slot; m_windowFailTotal = total; } //--- ResizeBuffers()/RefreshData() (Expert\AIBase\Features.mqh) size/refresh EVERY indicator //--- together each bar, including the 10 CFeatureBuilder now owns - direct calls into the owned //--- collaborator, no view needed (the signal always may call what it owns by value). bool FeatureVolumesBufferResize(const int n) { return m_featureBuilder.VolumesBufferResize(n); } void FeatureVolumesRefresh(void) { m_featureBuilder.VolumesRefresh(); } bool FeatureMaBufferResize(const int n) { return m_featureBuilder.MaBufferResize(n); } void FeatureMaRefresh(void) { m_featureBuilder.MaRefresh(); } //--- ONE LINE OF IDENTITY, for the census Warrior_EA.mq5 prints before it acts on g_aiSignals[]. //--- This makes them different. string RegistryLine(void) const { return StringFormat("%s | %s (%s) | era %d | %s%s", ID, m_activeFileName, (m_activeFileCommon ? "common" : "local"), (int)m_eraCount, (m_trainingComplete ? "deployed" : "training"), (m_ensembleMember ? StringFormat(" | ensemble member %d", m_ensembleIndex) : " | solo")); } //--- SHUTDOWN FLUSH: abandon an in-flight run instead of finishing it, and resume from the last //--- COMPLETED, already-persisted era. bool FlushTrainRun(void) { bool inFlight = (m_trainRunActive || m_eraResumePending || m_labelPrebuildActive || m_onlineLearning.SimRunActive()); m_trainingStopRequested = true; m_trainingPaused = false; //--- Drop the resumable bookkeeping WITHOUT calling FinalizeTrainRun: no checkpoint restore, no //--- persist, no dtStudied advance. The next start re-derives all of it from the saved model. m_trainRunActive = false; m_eraResumePending = false; m_haveOosCheckpoint = false; m_checkpointEra = -1; // the joint-checkpoint era stamp goes with the snapshot it describes m_labelPrebuildActive = false; m_onlineLearning.AbortSimIfActive(); //--- Leave the net in the same neutral state FinalizeTrainRun leaves it in - a frozen batch-norm or //--- a half-filled mini-batch must not be what a later inference path finds. Cheap, unlike the save. if(CheckPointer(Net) != POINTER_INVALID) { Net.SetBatchNormFrozen(false); Net.FlushBatch(); Net.SetBatchSize(1); } return inFlight; } void StopTraining(void) { m_trainingStopRequested = true; m_trainingPaused = false; //--- ScheduleTrainingIfNeeded() refuses to schedule another "New Bar" event while //--- m_trainingStopRequested is set, so a run interrupted mid-chunk would otherwise never get //--- called again to finalize (restore the best checkpoint, persist state) - do it //--- synchronously here instead. if(m_trainRunActive) FinalizeTrainRun(); Print(ID + ": training stopped by user (era " + IntegerToString(m_eraCount) + ", weights as of last completed era retained)"); PrintInferenceTally(); } //--- Inference-path census, printed at shutdown. Each implies a completely different fix. //--- Counting is the cheapest way to tell them apart and it costs nothing per bar. void NoteVoteGate(bool directional) { if(!directional) return; bool open = m_trainingComplete || (m_inferenceOnly && m_modelLoadedFromDisk); if(m_voteGateCompleteAtFirst < 0) { m_voteGateCompleteAtFirst = (int)m_trainingComplete; m_voteGateLoadedAtFirst = (int)m_modelLoadedFromDisk; } if(open) m_voteGatePassed++; else m_voteGateBlocked++; } void PrintInferenceTally(void) { long attempts = m_refreshOk + m_refreshFailFeatures + m_refreshFailShort; if(attempts <= 0) { Print(ID + ": inference census - RefreshLatestSignal was NEVER CALLED (0 attempts). The new-bar gate never fired."); return; } Print(ID + ": inference census - ", attempts, " refresh attempts: ", m_refreshOk, " completed, ", m_refreshFailFeatures, " bailed in BufferTempData, ", m_refreshFailShort, " bailed on a short feature window", " | decisions Buy:", m_refreshBuy, " Sell:", m_refreshSell, " Neutral:", m_refreshNeutral); //--- Second half of the census, and the half that separates "the model said nothing" from //--- "the model spoke and was not allowed to vote" - see m_voteGateBlocked for why that //--- distinction is the whole point. if(m_voteGateCompleteAtFirst < 0) Print(ID + ": inference census - vote gate was NEVER REACHED (no directional decision ever hit " "LongCondition/ShortCondition). Either every decision was Neutral, or this filter was never polled."); else Print(ID + ": inference census - vote gate passed:", m_voteGatePassed, " blocked:", m_voteGateBlocked, " | at first vote trainingComplete=", (m_voteGateCompleteAtFirst != 0 ? "true" : "false"), " modelLoadedFromDisk=", (m_voteGateLoadedAtFirst != 0 ? "true" : "false"), " inferenceOnly=", (m_inferenceOnly ? "true" : "false"), (m_voteGateBlocked > 0 && m_voteGatePassed == 0 ? " <-- EVERY directional call was discarded here. This is the zero-direction cause." : "")); } //--- The ONLY place the study event is posted: arms bEventStudy with THIS instance's id (so the //--- handler in OnChartEventHandler(), which matches on m_studyEventId, is the only member that //--- runs it) and stamps the lost-event watchdog. sparam tags ("New Bar"/"Init"/"Resume"/...) are //--- purely diagnostic. bool ArmStudyEvent(const long lparam, const string tag) { bEventStudy = EventChartCustom(ChartID(), m_studyEventId, lparam, 0, tag); if(bEventStudy) m_studyArmedTick = GetTickCount(); return bEventStudy; } void StartTraining(void) { if(!m_trainingStopRequested && !m_trainingPaused) return; m_trainingStopRequested = false; m_trainingPaused = false; if(!bEventStudy) ArmStudyEvent((long)dtStudied, "Resume"); Print(ID + ": training (re)started by user (era " + IntegerToString(m_eraCount) + ")"); } //--- Has an era ever cleared the per-class recall floor and been checkpointed this run? This is the //--- same quality bar the plateau ladder's auto-deploy requires (see PLATEAU_STAGE_DEPLOY), exposed so //--- the panel can warn before a MANUAL deploy ships a model that ignores Buy or Sell. bool HasRecallPassingCheckpoint(void) const { return m_bestPassedRecall; } //--- MANUAL deploy (panel "Deploy Model"): finalise whatever the run has found so far as THE //--- model - exactly what the plateau ladder does on its own at stage 3, just triggered early by //--- the operator. Reversible via RetrainDeployed(). bool DeployNow(void) { if(CheckPointer(Net) == POINTER_INVALID || !m_isInitialized) return false; if(m_trainingComplete) return true; // already deployed - nothing to do //--- Set BEFORE any save below: the flag is written INTO the .nnw, so persisting first would store //--- "still training" and a restart would resume the era loop instead of running the deployed model. m_trainingComplete = true; m_trainingPaused = false; m_trainingStopRequested = false; if(m_trainRunActive || m_haveOosCheckpoint) FinalizeTrainRun(); // restores the best checkpoint, persists, ends the run else { //--- Nothing trained this session (e.g. deploying a model that was just loaded from disk), so //--- there is no in-memory checkpoint to restore - persist exactly what is loaded right now. PersistDeployedModel(); SaveChartSignals(); } RefreshLatestSignal(); Print(ID + ": model DEPLOYED by user at era " + IntegerToString(m_eraCount) + " (selection score " + (m_bestSelectionScore < 0 ? "n/a" : DeployScoreText(m_bestSelectionScore)) + ", blended OOS " + DoubleToString(dOosForecast, 1) + "%) - training stopped, now running live inference" + (OnlineEnableLearningFlag() ? " with online continual learning" : "") + ". Use the panel's \"Retrain Model\" to resume training from here."); //--- Deliberately reported, not enforced: a manual deploy is the operator overriding the //--- ladder, and that override stays available. See DEPLOY_FAMILY_WISE_ALPHA and //--- HasRecallPassingCheckpoint()'s panel warning. ReportSelectionGateVerdict("manual deploy"); return true; } //--- The inverse of DeployNow(), and the ONLY way back: while m_trainingComplete is set, //--- ScheduleTrainingIfNeeded() routes every tick to the converged/inference branch, so //--- StartTraining() alone can never revive a deployed model (it clears the stop flag, but the //--- complete flag still wins that branch). void RetrainDeployed(void) { if(!m_trainingComplete) return; m_trainingComplete = false; m_trainingStopRequested = false; m_trainingPaused = false; //--- Persist the cleared flag immediately. Otherwise a terminal restart before the first era //--- completes would reload the .nnw still marked complete and silently go back to inference-only, //--- looking like the button did nothing. PersistDeployedModel(); if(!bEventStudy) ArmStudyEvent((long)dtStudied, "Retrain"); Print(ID + ": RETRAINING the deployed model from era " + IntegerToString(m_eraCount) + " - keeping its current weights as the starting point (use \"Delete & Reset Weights\" to start from scratch instead)."); } //--- Manual "rescan" of the drawn signal arrows: purges every arrow currently on the chart (namespaced //--- delete - user drawings untouched) and re-infers the last SIGNAL_RESCAN_LOOKBACK_BARS bars from the //--- CURRENTLY deployed weights, then re-runs the same end-of-era NMS declutter (PruneDirectionalClusters) //--- used during training so the fresh set matches what a live re-render would have produced. Wired to //--- the panel's Hide->Show Signals sequence: without this, "restore" only ever replays whatever was //--- last saved to the .arrows sidecar, which for a long-deployed model can be a stale historical render //--- from whenever it was last actually trained - years-old arrows crowding out anything recent. Chart-only //--- (no persistent chart in the tester/optimizer) and a no-op until a model has something to infer with. //--- This only does the cheap setup (buffer resize, arrow purge, cache alloc) and QUEUES the per-bar //--- inference loop for AdvanceChartSignalRescan() to drain in time-boxed slices off the timer - see //--- that method's comment for why the loop itself must never run in one blocking pass. Returns true //--- once a rescan has been queued (check RescanPending() for completion), false if there was nothing //--- to rescan (no deployed model, tester/optimizer context, etc). Body: Expert\Chart\ChartUI.mqh - //--- it manipulates the exact same rescan queue/tally AdvanceChartSignalRescan drains, so the two //--- halves of this state machine now live on the one object that owns the state. bool StartChartSignalRescan(void) { return m_chartUI.StartChartSignalRescan(); } //--- true while a queued rescan (StartChartSignalRescan above) still has slices left for //--- AdvanceChartSignalRescan to drain - polled by Warrior_EA.mq5's FinalizeSignalsRescanIfDone() to //--- know when it's safe to (re)apply arrow visibility and report the Show Signals click as complete. bool RescanPending(void) const { return m_chartUI.RescanPending(); } //--- forces a save of the network's current in-memory weights/state regardless of era-completion //--- state; called from OnDeinit() so shutdown/chart-removal never loses more than the current //--- tick of learning, and a subsequent restart's Train() resumes from m_eraCount rather than //--- the last fully-completed era only. bool PersistWeightsOnShutdown(void) { if(CheckPointer(Net) == POINTER_INVALID || !m_isInitialized) return false; //--- An inference-only run (any Strategy Tester pass - see m_inferenceOnly) trains NOTHING, //--- so there is no new state to persist and this save can only do harm. if(m_inferenceOnly) { PrintVerbose(ID + ": inference-only run - skipping the shutdown weight save (nothing was trained; the cached model is left exactly as seeded)."); return true; } //--- Nothing trained and nothing loaded => there is no state to persist, and writing anyway //--- is actively harmful. if(m_eraCount == 0 && !m_modelLoadedFromDisk) { PrintVerbose(ID + ": no era completed and no model loaded - skipping the shutdown weight save (leaving the model files absent so the next attach starts genuinely clean)."); return true; } //--- SKIP THE WEIGHT WRITE WHEN NOTHING CHANGED IT - see m_netDirty's declaration. For a //--- converged model with no online learning this is every save after the first, and it is //--- what keeps a six-chart terminal close from writing ~400MB of byte-identical .nnw files //--- into each other's OnDeinit budget. .stats is still written below: it is small and //--- carries state (the ensemble vote record, calibration) that changes without a weight. bool ok = true; bool wrote = m_netDirty; if(m_netDirty) { double currentIndicatorParams[]; m_indicatorTuner.Flatten(currentIndicatorParams); ok = Net.Save(m_activeFileName + ".nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, currentIndicatorParams); if(ok) m_netDirty = false; } else PrintVerbose(ID + ": weights unchanged since their last save - skipped the .nnw write (stats still saved)."); //--- calibration state (class priors + confidence scale) must travel with the weights so live //--- trading behaves like training after a restart - see SaveModelStats(). if(!SaveModelStats(m_activeFileName, m_activeFileCommon)) Print(ID + ": ERROR - shutdown SaveModelStats failed for " + m_activeFileName + ". Calibration state not persisted."); //--- Deliberately do NOT save the shadow net here. Worst case a shutdown loses only the //--- shadow's in-progress-era drift, which re-converges - a far better trade than risking the //--- whole model to an over-budget shutdown. if(!ok) Print(ID + ": ERROR - failed to persist weights on shutdown for " + m_activeFileName + ", error " + IntegerToString(GetLastError())); else if(wrote) PrintVerbose(ID + ": weights persisted on shutdown (era " + IntegerToString(m_eraCount) + ", trainingComplete=" + (string)m_trainingComplete + ")"); return ok; } //--- Persist the drawn arrows to disk, then remove THIS EA's chart visuals (arrows + status //--- label). Called early in OnDeinit(), before the heavy weight save, so a later stall/fault in //--- the save can never leave the chart littered. Deliberately NOT part of SaveWeightsNow(): a //--- mid-session manual save must not wipe the chart. void ShutdownChartCleanup(void) { PersistAndClearChartSignals(); } //--- Full shutdown persistence (weights + arrows), preserved for the panel's manual "save weights" //--- button (SaveWeightsNow) - does NOT purge the chart. OnDeinit no longer calls this; it runs //--- ShutdownChartCleanup() then PersistWeightsOnShutdown() so cleanup can't be starved by the save. bool PersistOnShutdown(void) { bool ok = PersistWeightsOnShutdown(); //--- Persist the drawn arrows too so a re-add/recompile restores them without a retrain. SaveChartSignals(); return ok; } //--- explicit manual save, identical persistence to PersistOnShutdown() but user-triggered from the panel bool SaveWeightsNow(void) { return PersistOnShutdown(); } //--- reloads this signal's current-config weights file from disk, discarding any unsaved in-memory //--- training progress since the last successful save bool LoadWeightsNow(void) { if(CheckPointer(Net) == POINTER_INVALID) return false; double loadedIndicatorParams[]; bool netLoaded = Net.Load(m_activeFileName + ".nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, loadedIndicatorParams); if(!netLoaded) { Print(ID + ": ERROR - failed to load weights from " + m_activeFileName + ".nnw, error " + IntegerToString(GetLastError())); return false; } m_modelLoadedFromDisk = true; //--- the file may carry a superseded architecture - correct it before anything reads the net EnforceTopologyContract(); //--- restore the calibration state that pairs with these weights (priors + confidence scale) so a //--- manual reload keeps live decisions calibrated exactly as the saved model was - see LoadModelStats(). LoadModelStats(m_activeFileName, m_activeFileCommon); if(ArraySize(loadedIndicatorParams) == AD_TUNE_PARAM_COUNT) { //--- same no-change guard as the resume path - see AdoptIndicatorParams if(m_indicatorsPtr != NULL) AdoptIndicatorParams(loadedIndicatorParams, m_indicatorsPtr); else m_indicatorTuner.Unflatten(loadedIndicatorParams); } //--- Discard resumable state: the load just swapped dtStudied/m_eraCount/weights out from under //--- whatever era a chunked Train() was mid-way through. m_trainRunActive = false; m_eraResumePending = false; m_haveOosCheckpoint = false; m_checkpointEra = -1; // the joint-checkpoint era stamp goes with the snapshot it describes m_oosWindow.Clear(); m_tuneTrialIndex = -1; RefreshLatestSignal(); Print(ID + ": weights reloaded from disk (era " + IntegerToString(m_eraCount) + ", trainingComplete=" + (string)m_trainingComplete + ")"); return true; } //--- deletes this signal's current-config saved files only (weights, topology config, in-progress //--- Deletes this config's saved files only and rebuilds a fresh untrained topology, so training //--- restarts from era 0. m_fileName embeds symbol+period+id+outputs+algo, so no other model's //--- files are reachable from here. bool ResetWeights(void) { bool stopped = m_trainingStopRequested; m_trainingStopRequested = true; // hold off any in-flight Train() scheduling while we reset //--- Whichever file this run trains against (see InitNeuralNetwork): the shared production //--- weights, or the tester cache during a backtest - so a panel reset mid-backtest cannot wipe //--- the live model. int flags = m_activeFileCommon ? FILE_COMMON : 0; string nnw = m_activeFileName + ".nnw"; string cfg = m_activeFileName + ".cfg"; string ckpt = m_activeFileName + "_ckpt.tmp"; //--- Sidecars pair with the weights being erased: .stats carries the calibration, _shadow.nnw the //--- deployed EMA, and _shadowclone.tmp is the clone staging file. Leave any behind and a fresh //--- retrain inherits the OLD model's calibration or blends into a stale shadow. string stats = m_activeFileName + ".stats"; string shadow = m_activeFileName + "_shadow.nnw"; string shadowClone = m_activeFileName + "_shadowclone.tmp"; //--- SAY WHAT HAPPENED TO EVERY FILE. This once printed only on a delete FAILURE, so a four-member //--- reset was 24 silent operations and "it only wiped the first model" could not be settled from //--- a log. "absent" on a member that should have had a .nnw is a different fault from "deleted". int filesDeleted = 0, filesAbsent = 0, filesFailed = 0; string wipeReport = ""; string targets[6]; targets[0] = nnw; targets[1] = cfg; targets[2] = ckpt; targets[3] = stats; targets[4] = shadow; targets[5] = shadowClone; for(int fi = 0; fi < 6; fi++) { ResetLastError(); string leaf = targets[fi]; string shortName = StringSubstr(targets[fi], StringLen(m_activeFileName)); // suffix only; full path prints below if(!FileIsExist(leaf, flags)) { filesAbsent++; wipeReport += StringFormat("%s%s=absent", (wipeReport == "" ? "" : " "), shortName); continue; } if(FileDelete(leaf, flags)) { filesDeleted++; wipeReport += StringFormat("%s%s=deleted", (wipeReport == "" ? "" : " "), shortName); } else { filesFailed++; wipeReport += StringFormat("%s%s=FAILED(%d)", (wipeReport == "" ? "" : " "), shortName, GetLastError()); Print(ID + ": ERROR - failed to delete " + leaf + ", error " + IntegerToString(GetLastError())); } } PrintFormat("%s: RESET WIPE of %s - %d deleted, %d already absent, %d FAILED | %s", ID, m_activeFileName, filesDeleted, filesAbsent, filesFailed, wipeReport); //--- The arrows and their .arrows sidecar belong to the model being erased, like the sidecars above. ClearPersistedChartSignals("weights reset from the panel"); m_eraCount = 0; m_trainingComplete = false; m_modelLoadedFromDisk = false; m_netDirty = true; // the on-disk copy is gone; whatever the net holds next must be written dtStudied = 0; dError = -1; dUndefine = 0; dForecast = 0; dPrevSignal = 0; m_nmsLiveBuyTime = 0; m_nmsLiveSellTime = 0; m_nmsLiveBuyAccept = false; m_nmsLiveSellAccept = false; m_nmsLiveKeptTime = 0; m_nmsLiveKeptDir = Neutral; m_nmsLiveKeptConf = 0; dOosError = -1; dOosForecast = 0; m_oosSamples = 0; //--- Lifetime counters: reset ONLY here. A normal restart restores them from .stats. m_cumIsCorrect = 0; m_cumIsTotal = 0; m_cumOosCorrect = 0; m_cumOosTotal = 0; if(m_ensembleMember) { g_ensCumOosCorrect = 0; g_ensCumOosTotal = 0; } //--- In-progress chunked run/tuning state references buffers and checkpoints from before the reset. m_trainRunActive = false; m_eraResumePending = false; m_haveOosCheckpoint = false; m_checkpointEra = -1; // the joint-checkpoint era stamp goes with the snapshot it describes m_oosWindow.Clear(); m_syncWaitStartTick = 0; m_tuneTrialIndex = -1; //--- Re-verify history sync and rebuild the label cache: both reference bars from before the reset. m_warmupPassesRemaining = 3; m_labelCacheBars = 0; m_labelCacheAnchorTime = 0; m_labelCachePrebuilt = false; m_labelPrebuildActive = false; m_prebuildSeedPending = false; m_sl_mode = SL_Mode; m_tp_mode = TP_Mode; m_onlineLearning.AbortSimIfActive(); //--- Salted with this model's id so two members reset in the same millisecond cannot collide, //--- and re-seeded so weight init is not dominated by the tuner's last candidate evaluation. WarriorRandSeed(ID); //--- RE-DERIVE THE SHAPE, in InitNeuralNetwork()'s order (window first - the other four read //--- what it measures). Until 2026-08-24 this rebuilt from whatever the members already held, //--- so a model whose window and capacity had been sized from a half-synced history stayed that //--- shape and was re-pinned to disk by the very action both fallback warnings tell the operator //--- to take. The .cfg was deleted above, so nothing on disk is being contradicted. m_topology.RemeasureSwingGeometry(); m_historyBars = DeriveHistoryBars(); m_convFilterCount = ComputeConvFilterCount(); m_lstmHiddenSize = ComputeLstmHiddenSize(); m_initialNeuronsCount = ComputeFirstLayerWidth(); m_hiddenLayersCount = ComputeHiddenLayerCount(); bool rebuilt = BuildFreshTopology(); if(!rebuilt) Print(ID + ": ERROR - failed to rebuild fresh topology after weights reset"); else { //--- isInitialized=FALSE, never m_isInitialized: every other writer runs before init sets it //--- true, so passing true here makes this the only .cfg on disk that fails its own compare //--- on the next attach. Runtime lifecycle state must never gate reuse. SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount, m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo, m_historyBars, m_outputNeuronsCount, m_neuronsCount, LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, false, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount, m_lstmHiddenSize, m_activeFileCommon); Print(ID + ": weights reset - training will restart from era 0 (current config only: " + m_activeFileName + ")"); } m_trainingStopRequested = stopped; if(!stopped && !bEventStudy) ArmStudyEvent(0, "Reset"); return rebuilt; } }; //+------------------------------------------------------------------+ //| IMPLEMENTATION. Method bodies, included after the declaration | //| above and nowhere else. Order between them is irrelevant. | //+------------------------------------------------------------------+ #include "AIBase\Training.mqh" #include "AIBase\Lifecycle.mqh" #include "AIBase\Topology.mqh" #include "AIBase\Labels.mqh" //--- Body in Expert\OnlineLearning\OnlineLearning.mqh. #include "AIBase\AutoTune.mqh" #include "AIBase\FeatureScreen.mqh" #include "AIBase\Inference.mqh" //--- Body in Expert\Persistence\ModelPersistence.mqh. //--- Now holds only InitOpen/InitClose/InitHigh/InitLow/InitTime/InitZigZag/ResizeBuffers/ //--- RefreshData - the rest of the old Features.mqh is in Expert\Features\FeatureBuilder.mqh. #include "AIBase\Features.mqh" #include "Training\AIBaseTrainingDataImpl.mqh" #include "Chart\AIBaseChartViewImpl.mqh" #include "Persistence\AIBasePersistenceViewImpl.mqh" #include "OnlineLearning\AIBaseOnlineLearningViewImpl.mqh" #include "Topology\AIBaseTopologyViewImpl.mqh" #include "Features\AIBaseFeaturesViewImpl.mqh" #include "ConfigLock\AIBaseConfigLockViewImpl.mqh" //+------------------------------------------------------------------+