//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //+------------------------------------------------------------------+ #include "ExpertSignalCustom.mqh" #include "..\AI\Network.mqh" #include "..\Variables\IndicatorResources.mqh" #include "..\Variables\IndicatorTuneRanges.mqh" #include "..\System\StatusLabel.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" #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_ensVoteWinLong[]; bool g_ensVoteWinShort[]; bool g_ensVoteDirLabel[]; // bar carried a Buy/Sell label - the coverage floor's base rate 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 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; //--- 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. #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 //--- 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 //--- Far below MI_NOISE_PERMUTATIONS because the profile redraws its null at EVERY lag, so cost is //--- draws x historyBars. 40 resolves p=0.05 to about one draw, and this figure never gates a trade. #define MI_LAG_PERMUTATIONS 40 //--- Per-lag significance, applied against the null of the MAXIMUM over lags rather than each lag's own. //--- The latter stars one lag per run before any signal exists - on SP500 H1 it produced two opposite //--- verdicts on identical data hours apart. #define MI_LAG_ALPHA 0.05 //--- WHICH TARGET BuildMiSample() scores against. "Optimal SL/TP" decomposes into HOW FAR price //--- travels (volatility - predictable) and WHICH barrier is reached first (direction - at the //--- noise floor). #define BARRIER_TARGET_RR_MIN 2.0 //+------------------------------------------------------------------+ //| Snap a raised ratio to a coarse shared ladder. PooledGate pools | //| instruments only when their structural break-even matches, and | //| continuous per-instrument ratios would never match, silently | //| emptying the pool. Snaps DOWN and is floored at the policy min. | //+------------------------------------------------------------------+ double BarrierSnapRr(const double wanted) { if(wanted >= 5.0) return 5.0; if(wanted >= 4.0) return 4.0; if(wanted >= 3.0) return 3.0; if(wanted >= 2.5) return 2.5; return BARRIER_TARGET_RR_MIN; } //+------------------------------------------------------------------+ //| The same ladder walked DOWNWARD, one rung per call. The raise | //| from the swing legs is only a PROPOSAL; DeriveBarrierGeometry | //| steps it down until the implied target is one the market | //| measurably reaches. Strictly decreasing, so the caller loop ends. | //+------------------------------------------------------------------+ double BarrierStepDownRr(const double rr) { if(rr > 4.01) return 4.0; if(rr > 3.01) return 3.0; if(rr > 2.51) return 2.5; return BARRIER_TARGET_RR_MIN; } //--- SCALE ladder for the stop quantile, walked WIDEST FIRST, taking the first rung whose implied target //--- is still reached often enough to be a trainable class. Without the reachability test this landed on //--- 6.66*ATR reached on 3.3% of bars - the model trained to predict something that never happened. #define BARRIER_SL_QUANTILE_COUNT 7 const double BARRIER_SL_QUANTILE_LADDER[BARRIER_SL_QUANTILE_COUNT] = {0.90, 0.85, 0.80, 0.75, 0.70, 0.60, 0.50}; //--- Fallback quantile when no rung clears the reachability floor. #define BARRIER_SL_QUANTILE 0.75 //--- ...and the target at the MEDIAN of favourable travel, so it is reached about half the time by //--- construction. The global derivation applies both once per era; CandidateGeometryFor applies the //--- same two per bar. Neither creates expectancy - what moves per candidate is the break-even. #define BARRIER_TP_QUANTILE 0.50 //--- Milliseconds the candidate-geometry measurement may spend inside one era's replay. A diagnostic //--- that freezes the chart is worse than a missing diagnostic; the report prints its own coverage. #define GEOMETRY_BUDGET_MS 5000 //--- Below this many resolved excursions the quantiles are too noisy to key a training target on. #define BARRIER_DERIVE_MIN_SAMPLES 500 //--- FIRST-PASSAGE LADDER. Recording first-touch AGE per rung makes any pair evaluable exactly, //--- with no re-walk. Barrier prices would need four ladders and bake in today's spread. #define BARRIER_LADDER_COUNT 14 const double BARRIER_LADDER[BARRIER_LADDER_COUNT] = {0.50, 0.75, 1.00, 1.50, 2.00, 3.00, 4.00, 5.00, 6.50, 8.00, 10.00, 13.00, 16.00, 20.00}; //--- The derivation is a FIXED-POINT ITERATION: the horizon scales with the target, and the excursions //--- are measured OVER that horizon, so target -> horizon -> excursions -> target is a loop. Deriving //--- once would set the target from travel measured under the OLD horizon. #define BARRIER_DERIVE_MAX_PASSES 5 #define BARRIER_DERIVE_TOLERANCE 0.05 //--- Reachability floor as a FRACTION OF BREAK-EVEN, not an absolute percentage: break-even for a //--- 1:RR trade is 100/(1+RR), so an absolute 20% is 0.60x break-even at RR=2 but 0.80x at RR=3 - //--- tightening the test simply because the user asked for a bigger target. #define BARRIER_MIN_REACH_FRACTION_OF_BE 0.60 //+------------------------------------------------------------------+ //| Reachability floor for one rung. A FUNCTION of the ratio, not a | //| constant, because the ratio is now per-rung - a floor computed | //| from one fixed RR would be the wrong strictness for every rung | //| the swing legs raised. | //+------------------------------------------------------------------+ double BarrierMinReachPct(const double rr) { return BARRIER_MIN_REACH_FRACTION_OF_BE * 100.0 / (1.0 + rr); } //--- WHICH END OF THE SCALE LADDER WINS. The two objectives are genuinely opposed, each correct in //--- its own phase. WIDE is right once an edge is KNOWN - EV = edge x width against a fixed spread. #define BARRIER_SCALE_MEASURE 0 // narrowest rung that stays cost-efficient - maximises detectability #define BARRIER_SCALE_DEPLOY 1 // widest rung that clears reachability - maximises EV per trade #define BARRIER_SCALE_OBJECTIVE BARRIER_SCALE_MEASURE //--- Stops MEASURE mode running to the tightest rung, mirroring the reachability floor that stops DEPLOY //--- mode running to the widest. Round-trip cost is 2*spread; at the measured 0.047*ATR that admits any //--- width down to ~3.1*ATR. #define BARRIER_MAX_COST_FRACTION_PCT 3.0 #define MI_TARGET_BARRIER 0 // shipped 3-class triple-barrier label #define MI_TARGET_EXC_UP 1 // (maxHigh - entry)/ATR over the horizon, 3 equal-frequency bins #define MI_TARGET_EXC_DOWN 2 // (entry - minLow)/ATR #define MI_TARGET_EXC_RANGE 3 // up + down: pure realised volatility, the control that SHOULD clear #define MI_TARGET_EXC_ASYM 4 // up - down: RAW asymmetry - CONFOUNDED BY VOLATILITY, see below //--- SCALE-FREE asymmetry, and the only one of the two that can support a directional claim. //--- Dividing by (up+dn) leaves the question actually being asked: given that price moved, which //--- way. #define MI_TARGET_EXC_ASYM_NORM 5 //--- 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 //--- WHAT THE TUNER OPTIMISES FOR. RANGE is the one target with measured signal (4x its null, //--- p=0.005, with a working positive control) and it is what the excursion head is trained to //--- predict. #define MI_TUNE_TARGET MI_TARGET_EXC_RANGE //--- Ceiling on profiled lags, sizing the retained-draw matrix. A larger m_historyBars is covered to the //--- first MI_LAG_MAX_PROFILE-1 lags and says so via the lag count it prints. #define MI_LAG_MAX_PROFILE 32 //--- Draws per candidate in the geometry scan. Above the ranking-only 20 because these draws also build //--- the FAMILY-WISE null, and a max-statistic lives in the upper tail where 20 draws are thinnest. #define MI_GEOMETRY_PERMUTATIONS 40 //--- Family-wise significance for the geometry winner. Stricter than MI_LAG_ALPHA because acting on it //--- means RELABELLING and retraining every topology from era 0. #define MI_GEOMETRY_ALPHA 0.05 //--- Ceiling on scanned candidates, sizing the fixed draw matrix; the loop still skips ineligible pairs. #define MI_GEOMETRY_MAX_CANDIDATES 12 //--- Standard errors a checkpoint's directional precision must clear chance by to be deployable. #define EDGE_MIN_SIGMAS 2.0 //--- Edge the geometry adoption must at least be ABLE to certify before switching. Deliberately generous //--- - 10pp over break-even is more than anything measured here - because the guard catches pairings //--- that are hopeless rather than merely hard. #define ADOPT_MIN_DETECTABLE_EDGE 0.10 //--- 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 //--- No oversampling constants: data-level class-balance oversampling went on 2026-07-31, and the //--- imbalance is corrected analytically in the gradient by the logit-adjusted loss. TRIPLE-BARRIER //--- LABELS (Lopez de Prado, ch. 3). #define BARRIER_TIE_GOES_TO_STOP 1 //--- Vertical (time) barrier, in bars. NOT in the weights fingerprint: a filename keyed on a //--- measured quantity orphans a trained model the moment the measurement moves. #define BARRIER_HORIZON_LADDER_COUNT 11 #define BARRIER_HORIZON_MIN 12 #define BARRIER_HORIZON_MAX 384 //--- Fallback when the ZigZag scan finds too few pivots for a median. Mid-ladder, and it logs when used. #define BARRIER_HORIZON_FALLBACK 32 //--- Confirmed pivots required before the median is trusted rather than the fallback. #define BARRIER_HORIZON_MIN_SAMPLES 20 //--- 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 //--- 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 //--- EXCURSION-SIZE HEAD (see AIBase\Excursion.mqh). Hidden width is deliberately small: the question //--- has a known low-dimensional answer (volatility clustering), and era time here is taken from a //--- classifier already at ~300 s/era. #define EXCURSION_HIDDEN_UNITS 24 //--- Below this many held-out bars the Brier skill score is noise and no verdict is printed. #define EXCURSION_MIN_SCORED 500 //--- Skill the head must beat before Stage 2 is justified. Not zero: replacing a constant that cannot //--- fail with a learned quantity that can must buy more than the era-to-era wobble of the estimate. #define EXCURSION_SKILL_USEFUL_PCT 2.0 //--- Minimum DISJOINT (non-overlapping-horizon) observations before the tally decides anything. //--- This used to be 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent //--- ones". #define EXCURSION_MIN_DISJOINT_SANITY 30 //--- Sigmas the paired per-window Brier difference must clear. Two-sided 2 sigma, the same bar every //--- other decision in this codebase is held to. Measured on DISJOINT windows, so no EffectiveSampleSize //--- deflation applies - striding by the horizon is precisely what buys that. #define EXCURSION_MIN_SIGMA 2.0 //--- Share of bars whose predicted survival curve is non-monotone. Above this the 8 sigmoids are not //--- describing one distribution and ExcursionQuantile's first-crossing read is undefined. #define EXCURSION_MAX_MONO_VIOL_PCT 5.0 //--- TRAILING-CLIMATOLOGY window over RESOLVED outcomes only - the real incumbent for "replace a global //--- ATR multiple", since a rolling rung frequency adapts to the regime and needs no model at all. The //--- head's margin over THIS, not over a frozen constant, is what would justify 760 inputs. #define EXCURSION_TRAIL_WINDOW 2000 //--- Resolved bars the trailing window must hold before its estimate may score anything. #define EXCURSION_TRAIL_MIN_N 500 //--- Train the head on one primary bar in this many: excursion targets are strongly autocorrelated, so //--- consecutive samples are near-duplicates, and this net's cost is per-dispatch rather than per-FLOP. #define EXCURSION_TRAIN_STRIDE 4 #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 //--- Balanced accuracy of a model that puts every bar in ONE class - the FLOOR of the metric, not a //--- midpoint. Any genuinely multi-class model scores above it. #define BALANCED_COLLAPSE_PCT (100.0 / 3.0) //--- How far above that floor a best-so-far checkpoint must sit before the regression handler defends //--- it: "still basically a collapse, keep exploring" versus "a real multi-class state we are sliding //--- off", the case that ran unchecked for 228 eras. #define BALANCED_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 balanced accuracy 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 //--- 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 confirmed swing leg, snapped DOWN to the ladder, capped. #define HISTORY_BARS_FALLBACK 20 #define HISTORY_BARS_FLOOR 12 #define WINDOW_DERIVE_SPAN_BARS 20000 #define WINDOW_DERIVE_MIN_LEGS 30 #define WINDOW_SWING_WING 12 //--- 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 //--- A class this rare cannot carry the per-class recall floor. NEUTRAL ONLY - the directional //--- classes are deliberately not exempted by prevalence. #define MIN_GATE_CLASS_SHARE_PCT 5.0 //--- 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 //--- 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); } //+------------------------------------------------------------------+ //| THE CANDIDATE-GEOMETRY SCAN'S OWN TALLIES. | //| | //| Paired per OOS call: the same bar scored under the global barrier | //| pair and under the pair this bar's excursion head would have | //| chosen, both resolved from the SAME first-passage ladder so the | //| difference cannot be an artifact of two evaluators disagreeing - | //| the f8ac10c mistake, one layer over. | //| | //| THESE TEN MUST BE CLEARED TOGETHER OR NOT AT ALL. They were ten | //| separate members, cleared in a ten-line block at era start and in | //| a second block on the shutdown-abort path - and that second block | //| cleared m_geoTrades alone, leaving nine partial sums behind. The | //| next era then divided a stale numerator by a restarted count. | //| Reset() exists so that arrangement cannot be written again. | //+------------------------------------------------------------------+ struct SGeometryScan { double diffSum; // sum of (candidate R - incumbent R) over paired calls double diffSumSq; // ...and of its square, for the paired sigma double incSum; // incumbent R total double candSum; // candidate R total int trades; // paired calls that resolved on BOTH geometries int incOpen; // unresolved inside the horizon, incumbent pair int candOpen; // ... and candidate pair; a resolution gap skews the means double candSl; // running mean of the chosen multiples, for the report double candTp; //--- Wall-clock guard. The scan adds a feature-window build and a head forward PER OOS call to a //--- walk that already runs unchunked at era end, on a single-threaded EA - the shape that got //--- the process force-terminated on 2026-08-21. It stops SCORING rather than stopping the walk, //--- so the replay is unaffected and the report says how many calls it covered. uint startTick; SGeometryScan(void) { Reset(); } void Reset(void) { diffSum = diffSumSq = incSum = candSum = 0.0; candSl = candTp = 0.0; trades = incOpen = candOpen = 0; startTick = 0; } }; //+------------------------------------------------------------------+ //| 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 balancedAcc; // the metric the checkpoint is selected on 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; balancedAcc = 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" #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" //--- AFTER BARRIER_LADDER, which its snap rule and its row width both read. #include "Training\FirstPassageLadder.mqh" #include "Training\MetaCandidateStore.mqh" //--- AFTER ShuffleRandomIndex, which its block permutation calls. It reads no member of this class //--- and no input, so nothing else constrains where it sits. #include "Training\FeatureSelector.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\TripleBarrier.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; } CiOpen m_Open; CiClose m_Close; CiHigh m_High; CiLow m_Low; CiVolumes m_Volumes; CiTime m_Time; //--- Optional classic-indicator input FEATURES, independent of the CSignal* instances used for //--- voting: feature engineering and signal voting are unrelated hierarchies. Periods come from the //--- tuner. Built-in Ci* wrappers throughout, so none of them carries a CiCustom depth limit. CiMA m_MA; CiRSI m_RSI; CiMACD m_MACDFeature; CiIchimoku m_Ichimoku; //--- Custom price-action/volume indicators (CustomIndicators\*.mq5), loaded via iCustom/CiCustom. CiCustom m_ADCumulativeDelta; CiCustom m_ADShorteningOfThrust; CiCustom m_ADWyckoffEventStream; CiCustom m_ADWyckoffFailedStructure; CiCustom m_ADWyckoffSignificantBarInversion; //--- "Is this AD indicator still calculating?" - cold must be a TRANSIENT rejection, never a //--- zero-fill; see the definition in Features.mqh. bool ADIndicatorCold(CiCustom &ind, string 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; //--- Last depth ServableBars() had to clamp to. Held so the explanation prints when the cap CHANGES //--- rather than once per era per call site. 0 = never clamped. int m_indicatorDepthCapBars; //--- One-shot latch for "an enabled tunable indicator reports NO calculated bars". Distinct from the //--- cap latch: that means "serves less than asked" and is recoverable, this means a dead handle with //--- no depth to clamp to. Cleared when a real depth returns, so a second outage is still reported. bool m_indicatorDepthDeadWarned; //--- Cooldown between attempts to rebuild a dead handle. Every ServableBars() consumer can reach the //--- repair - training, live inference, online learning - so a refusing terminal must not be hammered. uint m_handleRepairTick; //--- 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; //--- SettledBars() probe state. m_depthSettleStart doubles as the "a wait is in progress" flag. uint m_depthSettleStart; uint m_depthProbeTick; int m_depthProbeLast; int m_depthProbeStable; //--- 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_ADZigZag; //--- Live tunable values for each AD indicator plus their flatten/perturb/best-tracking logic. CADIndicatorTuner m_indicatorTuner; bool m_autoTuneIndicators; //--- RESEARCH ONLY - see the ExportFeaturesOnly input. Runtime replacement for the old //--- WARRIOR_EXPORT_FEATURES compile flag: read by InitNeuralNetwork() to call ExportFeatureMatrix() //--- instead of acquiring the config lock, and by Warrior_EA.mq5's OnTick() to skip trading entirely. bool m_exportFeaturesOnly; //--- Rebuilds only the AD* handles in place, so ReInit picks up updated param structs. bool ReInitADIndicators(CIndicators *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); //--- 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. bool BuildFreshTopology(); //--- Retained so TuneIndicatorsAndTrain() can call ReInitADIndicators() between trials. CIndicators *m_indicatorsPtr; CNet *Net; //--- EMA "shadow" copy of Net, blended a SHADOW_WEIGHT_TAU step toward Net at the end of every //--- era rather than replaced. Live inference reads THIS, so any single era's raw weights - //--- including an Adam overshoot - can only nudge what is deployed, never overwrite it. CNet *m_shadowNet; //--- One-shot latch for the clone bootstrap. Cloning a second net can fail on the tester's CPU- //--- DLL fallback, and without this the retry would re-initialise the compute backend on EVERY //--- bar. bool m_shadowBootstrapAttempted; //--- ONLINE CONTINUAL-LEARNING STATE (see OnlineLearnStep(); tunables at ONLINE_LEARN_*). The //--- watermark is a bar TIME, not a now-relative index, so it survives the per-bar index-frame //--- shift. bool m_enableOnlineLearning; datetime m_onlineLearnedUpToTime; double m_onlineRollingAcc; long m_onlineSamples; int m_onlineBarsSincePersist; //--- Latched log state so the guardrail freeze/resume transition prints once per flip, not per bar. bool m_onlineBlendFrozen; 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; //--- 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; 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; //--- minimum acceptable OOS recall (%) for the Buy and Sell classes individually before Train() is //--- allowed to declare convergence; a class with zero OOS samples this era doesn't block (avoids a //--- deadlock when a given era's OOS window happens to contain no examples of that class) int m_minDirectionalRecallPct; //--- THE OPERATIVE per-class floor, derived from the class's own effective sample (the input //--- above is only the fallback when the sample is too thin for an SE). double CollapseRecallFloorPct(int classTrueCount); //--- Last era-progress Print for THIS member (see the era loop's 5s rate limit). uint m_lastProgressLogTick; //--- THE EXIT POLICY ACTUALLY IN FORCE, pushed in from the same inputs the live path reads //--- (Signal_ThresholdClose / HoldToBarrier). 0 = no vote-driven exit, which is what ships //--- today. double m_exitVoteThreshold; bool m_exitHoldToBarrier; //--- The model's ADJUSTED signed decision per OOS bar, captured during pass 3 - the same value //--- that votes live, 0 where it abstains. double m_oosDecisionSeries[]; //--- The trade the EA would ACTUALLY have taken from `entryIdx`, under the policy above: first //--- of stop / target / vote reversal / horizon. bool SimulateTradeOutcome(int entryIdx, bool isLong, double &rMultiple, int &lifespanBars, bool &endedOnVote, bool &endedOnTimeout); //--- Per-era accumulators for the simulated-exit report (see ReportExitPolicyDivergence). double m_simRSum; double m_simRSumSq; //--- Trades that reached NEITHER barrier inside the horizon, and what they actually paid. These //--- are what CostAdjustedBreakEvenPct does not know about - see EmpiricalBreakEvenPct. int m_simTimeouts; double m_simTimeoutRSum; //--- The LAST COMPLETED era's timeout measurement. Era N therefore reports against era N-1's //--- measurement, which is the honest pairing anyway: a partial era is not one. double m_lastTimeoutShare; // t in 0..1; -1 = never measured double m_lastTimeoutMeanR; // mean R of the trades that timed out int m_simTrades; int m_simVoteExits; int m_simBarrierWins; //--- The SIMULATION's own target-before-stop count, beside the LABEL's above. Two walks, and the //--- report used to quote only the label's while quoting the simulation's expectancy - so a //--- disagreement between them read as a property of the trade rather than of our arithmetic. int m_simTpHits; //--- See SGeometryScan: ten tallies that must be cleared together. SGeometryScan m_geo; //--- The pair this bar's excursion head would choose. False when the head cannot answer. bool CandidateGeometryFor(const int barIdx, const bool isLong, int &slRung, int &tpRung); void ScoreCandidateGeometry(const int barIdx, const bool isLong); void ReportCandidateGeometry(void); bool m_exitReplayReported; void ReportExitPolicyDivergence(void); //--- Replays every directional call of this era under the live exit policy. Runs AFTER pass 3, never //--- inside it: a vote-flip exit for a trade at bar r depends on the decisions at bars r-1, r-2, ... //--- which pass 3 has not produced yet when it grades r (it walks oldest-to-newest). void SimulateExitPolicyOutcomes(void); //--- ExitPolicy() itself is PUBLIC (with the other Warrior_EA.mq5 setters) - it is pushed in //--- from the EA, not called from inside the class. These two are what it writes. double m_lastRecallFloorPct; //--- Installs an adopted barrier geometry and every side effect that must travel with it: the //--- derived pair (the one authority), the legacy mode ints, the live-order globals, the .cfg //--- rewrite, the label cache and the horizon latch. void ApplyAdoptedGeometry(double sl, double tp, int slMode, int tpMode); //--- 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); bool m_detectabilityReported; //--- 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; //--- THE class-imbalance correction: tau in Menon et al. 0 disables it. double m_logitAdjustTau; 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[]; 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; //--- META GATE telemetry, read by the HUD's meta line in ChartUI.mqh. The armed latch is //--- maintained by MetaGateArmedNow() below (any observer may refresh it); the score/counter //--- fields are written ONLY by the meta head's own scorer on LIVE queries (barIdx 1) - the //--- ensemble verdict's historical replays must not inflate the live approve/veto tally. //--- NOT m_metaGate: that name belongs to the CMetaGate* the root signal owns, one class up. //--- What this holds is the RECORD of what a gate did, which is a different thing. SMetaGateTelemetry m_metaTelemetry; //--- The ONE readiness test + transition latch for the meta gate: the same test that lets a //--- direction member's vote count (VoteCapableWeight) - a converged training run on this chart, //--- or a model loaded for inference-only duty. bool MetaGateArmedNow(void) { if(!IsMetaTarget()) return false; bool armed = (CheckPointer(Net) != POINTER_INVALID && (m_trainingComplete || (m_inferenceOnly && m_modelLoadedFromDisk))); if(m_metaTelemetry.SetArmed(armed)) { Print(ID + (armed ? ": META GATE ARMED - vote-cleared entries are now scored against the" " cost-adjusted break-even (" + DoubleToString(CostAdjustedBreakEvenPct(), 1) + "%); below it the entry is vetoed. Scores are the trained context trunk with" " the pattern term zeroed - ranking quality, not calibrated probability; see" " CSignalMETA::ScoreProposal's header." : ": META GATE DISARMED - the head is (re)training; entries pass ungated.")); } return armed; } //--- 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, in bars. The stock ZigZag revises its most recent 1-3 legs as //--- new bars arrive, so a bar's buffer value is trusted only once this many MORE bars have //--- closed after it. It used to gate the LABELS too, back when the target was the exact //--- confirmed pivot; the target is now the triple barrier, whose lookahead is its own horizon. int m_swingConfirmationBars; //--- Vertical barrier of the triple-barrier label, in bars - see BARRIER_HORIZON_LADDER_COUNT. //--- Derived once by ComputeBarrierHorizonBars() at the start of the label prebuild and then //--- held for the run. int m_barrierHorizonBars; //--- Latch so the invalid-TP fallback in BarrierMultiples() shouts once, not once per labelled bar. bool m_barrierFallbackWarned; //--- Did the LAST TripleBarrierLabel() call run out of horizon without either barrier being //--- touched? bool m_lastBarrierTimedOut; //--- The LAST walk was cut short by the SCHEDULED CLOSE-ALL vertical barrier (the weekend flat //--- rule) rather than running its full horizon. bool m_lastLabelWeekendCut; //--- Did the LAST call find BOTH directions' targets reachable inside the horizon? It is not an //--- edge case there: it is the whipsaw class, and on SP500 H1 it accounts for nearly all of //--- Neutral. Published so the prebuild can count it - see m_labelPrebuildBothWonCount for why a //--- bar that wins in either direction must not be labelled "do not trade". bool m_lastBarrierBothWon; //--- Subset of the above where both targets fell inside the SAME bar, so OHLC cannot say which came //--- first. Those stay Neutral, for the same reason intrabar ties score as the stop: the file refuses //--- to order two touches it cannot see the order of. bool m_lastBarrierBothWonTied; //--- Did a LONG / a SHORT placed at this bar reach its target before its stop? See //--- m_oos.winLongTotal for why the deploy gate had to stop using label agreement as its hit //--- test. bool m_lastWinLong; bool m_lastWinShort; //--- Excursions of the bar TripleBarrierLabel() just resolved, in ATR units, published the same way //--- m_lastBarrierTimedOut is: the walk that finds them is the walk the label already does, so they //--- cost one max and one min per bar rather than a second pass over history. double m_lastExcUp; // (maxHigh - entry)/ATR over the horizon, >= 0 double m_lastExcDown; // (entry - minLow)/ATR, >= 0 //--- SIGNED close-to-close travel at the last bar the walk actually visited, in ATR and BEFORE //--- the spread: what a trade still open when the horizon (or the scheduled close-all) ran out //--- would be marked at. Positive = price above the entry bar's close. A trade that reaches //--- neither barrier is NOT worth zero - it is closed at this mark, which is what the live //--- close-all does and what SimulateTradeOutcome's timeout path already charges. double m_lastTermTravel; //--- LABEL LIFESPAN in bars: how long after entry this bar's label became KNOWABLE - the age of //--- the barrier touch that fixed the outcome, or the full horizon on a timeout. NOT a //--- diagnostic. See EffectiveSampleSize(). int m_lastLabelLifespan; //--- 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; //--- The ratio the derivation actually landed on (target/stop). At or above BARRIER_TARGET_RR_MIN //--- by construction; PooledGate keys poolability on it, since it IS the structural break-even. double TargetRR(void) const { return (m_derivedSlMult > 0.0) ? (m_derivedTpMult / m_derivedSlMult) : BARRIER_TARGET_RR_MIN; } //--- DERIVED barrier multiples, in ATR units, taken from the measured excursion distribution //--- rather than from an enum. Zero means "not derived yet" and BarrierMultiples() falls back to //--- the mode constants. double m_derivedSlMult; double m_derivedTpMult; bool m_geometryDerived; //--- 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; int m_geometryDerivePasses; // fixed-point iteration counter, capped //--- The last ComputeBarrierHorizonBars() ran with FEWER confirmed ZigZag legs than the median //--- needs, so the horizon it returned is the fallback, not a measurement. bool m_barrierHorizonLegStarved; bool m_horizonStarvedWarned; // one-shot: the starved path can retry every call //--- Has THIS process written the derived geometry into the .cfg? Set by the post-derivation //--- save, and also by the adoption path (the pair is already on disk there). bool m_geometryCfgSaved; //--- ADOPTED-GEOMETRY LATCH. True once ReportBarrierGeometryScan has crowned a pairing that //--- cleared its family-wise null. The scan wins, and this latch is how. So the scan's decision //--- was inert, and had it not been it would have been overwritten by the next derive pass //--- anyway. bool m_geometryAdopted; //--- MEASURED DIRECTIONAL EVIDENCE, and the one thing that makes the MI suite a SCREEN rather //--- than a commentary. bool m_dirEvidence; string m_dirEvidenceWhy; //--- Median confirmed ZigZag leg in bars, UNSCALED by the barrier. The window excursions are measured //--- over, kept independent of the geometry so sizing the geometry from them cannot feed back. int m_swingMedianBars; //--- Median confirmed ZigZag leg RANGE, in ATR units - the price twin of m_swingMedianBars, and //--- measured in the same pivot scan (ComputeBarrierHorizonBars) so the two describe the same //--- legs. double m_swingMedianLegAtr; int m_labelPrebuildTimeoutCount; int m_labelPrebuildWeekendCutCount; //--- Bars where BOTH targets were reached, and the same-bar subset that could not be ordered. int m_labelPrebuildBothWonCount; int m_labelPrebuildBothWonTieCount; //--- 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; //--- EXCURSION-SIZE HEAD. Kept separate, the classifier is bit-for-bit unaffected and this whole //--- instrument is removable without trace. CNet *m_excNet; bool m_excHeadFailed; // one-shot: creation failed, do not retry every bar //--- Allocated once, reused every bar. getResults takes CArrayDouble*& and allocates when handed a //--- NULL, so locals would mean an allocation per bar across ~32k bars an era. CArrayDouble *m_excTgt; CArrayDouble *m_excOut; long m_excBaseHits[2 * BARRIER_LADDER_COUNT]; long m_excBaseTotal; // rows the base rates were estimated from double m_excBrierHead[2 * BARRIER_LADDER_COUNT]; double m_excBrierBase[2 * BARRIER_LADDER_COUNT]; int m_excScored; // held-out bars scored this era //--- Since e2c9593 every scored bar IS a disjoint window (the score step strides by the //--- horizon), so m_excBrierHead/m_excOosHits are already the disjoint tally and m_excScoredD //--- just counts it. double m_excBrierHeadT[2 * BARRIER_LADDER_COUNT]; int m_excScoredD; //--- PAIRED PER-WINDOW BRIER DIFFERENCES over the decision rungs - base minus head, and trail //--- minus head - one value per DISJOINT window. double m_excDiffSum; double m_excDiffSumSq; double m_excTrailDiffSum; double m_excTrailDiffSumSq; //--- Which ladder rungs bracket the live SL/TP, i.e. the ones ExcursionQuantile would actually read. //--- ONE definition, called by both the scorer and the report - they disagreed silently the moment //--- there were two copies of the bracketing test, and the scorer's copy decides what the report's //--- standard error is computed over. void DecisionRungMask(bool &mask[]); //--- OOS positives per rung. Feeds the ORACLE control: the best constant achievable ON THE SCORED //--- BLOCK, in closed form. Separates "predicts per bar" from "learned a level nearer the OOS rate //--- than the frozen IS constant", which scores positive while carrying no per-bar information. long m_excOosHits[2 * BARRIER_LADDER_COUNT]; //--- Bars whose predicted survival curve rose with distance. P(reach k) must be non-increasing in k; //--- nothing constrains 8 independent sigmoids to obey that, and ExcursionQuantile reads the first //--- crossing, so a tangled curve is misread exactly where the head is least certain. int m_excMonoViol; long m_excTrainTick; // stride counter, on attempts not acceptances ulong m_excUs; // head's own microseconds this era - see the era line //--- TRAILING CLIMATOLOGY (see EXCURSION_TRAIL_WINDOW). Ring of per-bar outcome bitmasks - 32 //--- rungs fit one ulong, so the whole rolling history is one array of longs. ulong m_excTrailRing[]; int m_excTrailHead; // next write position int m_excTrailCount; // entries pushed so far, capped at the ring size long m_excTrailHits[2 * BARRIER_LADDER_COUNT]; long m_excTrailN; // resolved bars currently inside the window double m_excBrierTrail[2 * BARRIER_LADDER_COUNT]; long m_excTrailScored; // bars scored while the trailing estimate was usable 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; //--- Balanced accuracy (macro-recall: mean of Buy/Sell/Neutral OOS recall) of the era the //--- current checkpoint was taken from. double m_bestBalancedOos; //--- whether the era m_bestOosForecast/the checkpoint was taken from also cleared the per-class //--- directional recall floor (see directionalRecallOK below) - part of the "best" ranking itself, //--- not just a side note, so blended accuracy alone can never outrank a directionally-useful era //--- (see the checkpoint/g_eta-decay comment in Train()'s era-end block for why that matters). 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 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_bestBalancedOos 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_erasSinceBestBalanced counts eras //--- since the last NEW BEST balanced accuracy; m_plateauStage is how far up the escalation it //--- has climbed. int m_erasSinceBestBalanced; 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; //--- fractal/swing-confirmation/trend-context Buy/Sell label cache: the label at a given now- //--- relative bar index only depends on price/ATR history, never on model state, so recomputing //--- it every era (as opposed to once per real bar close) is pure waste. double m_excUpCache[]; double m_excDownCache[]; //--- FIRST-PASSAGE LADDER - the per-rung first-touch ages and the terminal travel, published //--- under the SAME validity flag as the two excursion caches above (see m_lastTermTravel). It //--- owns those three arrays now, so the bounds test and the log-space rung snap exist once //--- instead of at four and three call sites - see Training\FirstPassageLadder.mqh. CFirstPassageLadder m_ladder; //--- Scratch for the bar TripleBarrierLabel is currently walking, published the same way m_lastExcUp //--- is and copied into the caches by AdvanceBarrierLabelState under the label's validity flag. int m_lastLadderUpAt[BARRIER_LADDER_COUNT]; int m_lastLadderDownAt[BARRIER_LADDER_COUNT]; //--- Reports expectancy for every ladder pair - see the definition. Measurement only; it does not //--- (yet) choose the geometry. void ReportGeometryExpectancyScan(void); bool m_labelCacheBuy[]; bool m_labelCacheSell[]; //--- Per-bar outcome of each DIRECTION taken on its own, cached beside the label under the same //--- m_labelCacheHasValue flag. This is what the deploy gate scores against - see //--- m_oos.winLongTotal. bool m_winLongCache[]; bool m_winShortCache[]; 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 void ComputeLabelForBar(int i, int bars, bool &buy, bool &sell); void AdvanceBarrierLabelState(int i, int bars); //--- The triple-barrier verdict for one bar - the training TARGET. ENUM_SIGNAL TripleBarrierLabel(int idx); //--- First scheduled close-all strictly after `after`, 0 when the schedule is disabled - the //--- label walk's second vertical barrier. Body beside TripleBarrierLabel in AIBase\Labels.mqh. datetime NextScheduledCloseAll(const datetime after); //--- MEASURED bars between consecutive scheduled close-alls, and the mean an entry actually gets. //--- The horizon ladder is capped by BARRIER_HORIZON_MAX, which the close-all makes fiction: no //--- trade survives one cycle, whatever the ladder granted. Diagnostic only for now. int MeasureCloseAllBudget(int &meanBudgetBars); //--- BARRIER_HORIZON_MAX, lowered to what the scheduled close-all actually grants. THE horizon //--- ceiling from 2026-08-22 on: every label timeout on both live charts was the close-all and //--- none was the horizon, so the old ceiling never bound anything. Measured once, then cached - //--- the scale ladder asks per rung. int EffectiveHorizonMax(void); int m_closeAllCycleBars; // 0 = not measured yet, -1 = no schedule int m_closeAllMeanBudget; //--- Resolves the SL/TP ATR multiples the label uses from the EA's live SL_Mode/TP_Mode. Split out //--- because the INTELLIGENT modes scale with AI confidence, which does not exist at label time - //--- see the definition for why the label uses their zero-confidence base instead. void BarrierMultiples(double &slMult, double &tpMult); //--- Bars this (sl, tp) pair needs before its label means "target before stop" rather than //--- "target before stop OR 384 bars, whichever comes first". int RequiredHorizonBars(double slMult, double tpMult); //--- Bars it would actually GET: the above, clamped to [MIN, MAX] and snapped DOWN to the //--- ladder. These are two different numbers and conflating them is its own trap - the ceiling //--- clamp is what ReportGeometryExpectancyScan disqualifies with '!', but the snap-down //--- truncates as well and is silent about it (a pair needing 317 bars is granted 256). int SnapHorizonToLadder(int rawBars); int GrantedHorizonBars(double slMult, double tpMult); //--- Independent-observation count behind `rawN` overlapping triple-barrier 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, i.e. the old behaviour, 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; //--- Fills this instrument's record from its own geometry and hands it over. Stays here because //--- only this class knows its symbol, its ratio 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; //--- The ACTUAL ratio, not the policy floor: since 2026-08-19 the swing legs may raise it per //--- instrument, and the ratio IS the structural break-even this record is pooled on. rec.targetRR = TargetRR(); 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(TargetRR(), 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; //--- Break-even WITH the spread, which is the bar a model actually has to clear. A win nets (TP //--- - spread), a loss costs (SL + spread). Falls back to frictionless when m_spreadAtr is unset //--- (a loaded model that has not re-derived). double CostAdjustedBreakEvenPct(void); //--- Spread in ATR units, averaged over the IS bars - measured in ReportGeometryExpectancyScan, which //--- is the only place with both the ATR series and the label cache in hand. 0 = not yet measured. double m_spreadAtr; //--- Median confirmed-ZigZag-leg length over the training window, snapped to the horizon ladder. int ComputeBarrierHorizonBars(int bars); //--- Resolves m_barrierHorizonBars exactly once per process, from live buffers. Needed on BOTH //--- paths, which is the whole reason it is not simply inlined in the prebuild: a DEPLOYED model //--- never enters Train(), so it never reaches StartLabelCachePrebuild() - yet OnlineLearnStep() //--- reads the horizon as its confirmation delay. void EnsureBarrierHorizon(int bars); bool m_barrierHorizonResolved; //--- 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. bool m_featureFailTransient; //--- WHICH BLOCK rejected the bar, and at which series index. 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. int m_windowFailSlot; int m_windowFailTotal; bool m_featureWidthWarned; // one-shot: the width contract is a structural fault //--- 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); bool m_featureHealthReported; bool BufferTempDataCompute(int 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); //--- 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: once the core model converges, a CLONE //--- of its weights (never the production Net itself) walks forward through the OOS window bar- //--- by-bar, scoring each bar with its current weights THEN learning from it - simulating how //--- the model would adapt in live/forward trading. CNet *m_simOosNet; // NULL when no simulation is active bool m_simOosRunActive; int m_simOosCutoff; // oosCutoff snapshot from the run that converged int m_simOosBarIndex; // resume point, m_simOosCutoff-1 down to 0 double m_simOosForecast; // smoothed accuracy - separate from dOosForecast int m_simOosSamples; void StartOosContinualSimulation(int bars, int oosCutoff); void AdvanceOosSimulationChunk(void); //--- ONE-SHOT pattern-database backfill (user request 2026-08-16): "the DB needs to be filled //--- during training so I do not have to run a backtest before deploying to live trading". bool m_dbBackfillActive; bool m_dbBackfillDone; // one-shot per deployment - never re-armed by a later call int m_dbBackfillIndex; // resume point, descends to 2 (mirrors pass 3's m_oosScoreIndex) int m_dbBackfillStartIndex; int m_dbBackfillStopIndex; // inclusive floor - the ranking slice's newest bar int m_dbBackfillBars; int m_dbBackfillFired; // rows written, for the completion log line long m_dbBackfillEra; // era stamped into the .dbfill marker on completion void StartPatternDatabaseBackfill(int bars, int totalIter, int oosCutoff); void AdvancePatternDatabaseBackfill(void); //--- 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, int featureBarOffset = 0, int target = MI_TARGET_BARRIER); //--- Is an "optimal SL/TP" head learnable? Scores the features against excursion magnitude and //--- asymmetry instead of the barrier class - a different question, see the definition. void ReportExcursionInformation(void); //--- Sets the ATR multiples from the measured MFE/MAE quantiles instead of the mode enums. Returns //--- false (and leaves the configured pair standing) when there are too few resolved excursions. bool DeriveBarrierGeometry(void); //--- LAG PROFILE: how far back the features still say anything about the entry they precede. WHY //--- THIS WAS MISSING AND WHY IT MATTERS: BuildMiSample samples ONE bar. int ReportFeatureLagProfile(void); //--- 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 horizon/4. int MiShiftPad(void) const { //--- Also covers m_historyBars, because the lag profile shifts the FEATURES that far back and every //--- build must still enumerate the identical bar set (see BuildMiSample's fixed-pad note - padding //--- by the requested offset instead is what voided the positive control on 2026-08-02). return MathMax((int)MathMax(m_historyBars, 0), MathMax(MI_ALIGN_MAX_SHIFT, MathMax(m_barrierHorizonBars, 1) / 4)); } double ScoreMiSample(const double &cols[], int &labels[], int n, bool shuffleLabels); //--- The same scoring, additionally handing back the PER-COLUMN vector it computes on the way to //--- the mean. The four-argument form above is this one with a scratch array: one arithmetic, so a //--- caller that wants the columns and one that wants the mean can never be measuring two things. double ScoreMiSample(const double &cols[], int &labels[], int n, bool shuffleLabels, double &perColumn[]); //--- 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); //--- Same number, plus HOW MANY tunable indicators were actually consulted. int TunableBarsCalculated(int &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); //--- `want`, clamped to what the indicators can actually serve. THE single gate in front of //--- every ResizeBuffers() call site (train, live inference, chart rescan, research export). int ServableBars(int want, string 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); //--- Per-indicator BarsCalculated(), for the cap/priming/stall lines. It answers that directly. //--- Which BLOCK a feature slot belongs to, e.g. "spread[1]". string FeatureSlotName(const int slot); string IndicatorDepthReport(void); //--- 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); int NoteHandleMove(const string name, const int oldHandle, const int newHandle, string &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; //--- Ranks every selectable SL/TP pairing by how much the SAME features say about THAT barrier //--- outcome at ENTRY time - see the definition. Read-only: it relabels a sampled copy, never the //--- label cache, and restores the barrier state it borrowed. void ReportBarrierGeometryScan(void); //--- Scan overrides consulted by BarrierMultiples(). Both > 0 or neither applies; 0 = off. Live only //--- for the duration of ReportBarrierGeometryScan, and nothing persisted is keyed on them. double m_barrierScanSlMult; double m_barrierScanTpMult; //--- true => BuildMiSample computes each label with TripleBarrierLabel() instead of reading the cache, //--- because a hypothetical geometry's labels are by definition not cached. bool m_barrierScanLiveLabels; //--- timed-out labels seen during one geometry's live relabel - see the scan's dir/to columns. int m_barrierScanTimeouts; //--- set by ComputeBarrierHorizonBars: this geometry needs MORE time than BARRIER_HORIZON_MAX allows, //--- so its label truncates a trade the EA would hold to SL/TP. Disqualifies it from the scan. bool m_barrierHorizonClamped; double m_miBestColumn; double m_miLabelEntropy; //--- 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 barrier horizon //--- move together, so THIS - not the row count - is the sample size the p-value really rests on). int m_miNullBlocks; //--- PER-COLUMN verdict from the same draws the headline null above is built from. m_miBestColumn //--- keeps only the strongest column; this keeps every column, which is what names what to cut. //--- REPORT-ONLY until a report has actually been read - see ReportFeatureLabelInformation(). CFeatureSelector m_featureSelector; 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); //--- 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; //--- 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; //--- Name of the terminal-wide global variable this instance holds as an exclusive claim on //--- m_activeFileName, or "" when it holds none. See AcquireConfigLock(). string m_configLockName; //--- 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 //--- +/-WINDOW_SWING_WING bars, alternation enforced - snapped down to {12,16,20,24,32}. int DeriveHistoryBars(void); 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; //--- TRAINING TARGET (Meta_Labeling_Design.md). Never mutated after configuration - it feeds the //--- fingerprint like any other identity-defining member. int m_trainTarget; //--- 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; 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; //--- CONDITIONAL leg excursions for the fractal target's geometry (user request 2026-08-15: //--- "calculate MAE and MFE from a fractal to the next"). Safe against circularity ONLY because //--- the fractal label does not depend on SL/TP (the barrier label does - never feed it this //--- path). double m_fracLegFav[]; double m_fracLegAdv[]; int m_fracLegCount; void RecordFractalLegExcursion(const double fav, const double adv) { if(fav <= 0.0 && adv <= 0.0) return; int cap = ArraySize(m_fracLegFav); if(m_fracLegCount >= cap) { cap += cap / 2 + 256; ArrayResize(m_fracLegFav, cap); ArrayResize(m_fracLegAdv, cap); } m_fracLegFav[m_fracLegCount] = MathMax(fav, 0.0); m_fracLegAdv[m_fracLegCount] = MathMax(adv, 0.0); m_fracLegCount++; } //--- This member's slot in the combined ensemble panel; claimed lazily on first publish (-1 = none). int m_ensemblePanelSlot; //--- Meta candidate store for the CURRENT era's bar grid, populated by MetaPrepareEra() //--- (overridden in CSignalMETA; empty and unused for direction models). CMetaCandidateStore m_metaCands; //--- Candidate id for each pass-2 queue slot, parallel to m_isTrainQueue (see Training.mqh's //--- queueing block); -1 on every slot for direction models. Swapped in lockstep by the shuffle. int m_isTrainQueueCand[]; //--- Per-family (0-3) and per-side (0=long 1=short) OOS decomposition of the meta head's era - //--- candidates / base wins / operating-point trades / wins among trades. Reset each era beside //--- m_oos.buyFired. int m_metaFamCand[4], m_metaFamWins[4], m_metaFamFired[4], m_metaFamFiredWins[4]; int m_metaSideCand[2], m_metaSideWins[2], m_metaSideFired[2], m_metaSideFiredWins[2]; //--- 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.) bool AddConvStage(CArrayObj *topology); bool AddLstmStage(CArrayObj *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; } //--- META-TARGET SEAMS (all no-ops for direction models; overridden only by CSignalMETA). The //--- rest of the seams stay here. FRACTAL TARGET: direction to the next confirmed fractal //--- extreme on every bar, ~balanced by construction. bool IsFractalTarget(void) const { return m_trainTarget == 2; } //--- Labels.mqh: fractal-direction label for one bar (overrides the barrier verdict in //--- AdvanceBarrierLabelState when IsFractalTarget()). ENUM_SIGNAL FractalDirectionLabel(int idx); //--- Resolve the candidate corpus onto this era's bar grid (fills m_metaCands). Called at //--- every era start, right after the bar grid is sized; returning false aborts the training run. virtual bool MetaPrepareEra(const int bars) { return true; } //--- Append the per-candidate setup descriptor to TempData, AFTER BuildFeatureWindow() has filled //--- the shared bar window. The input layer is sized historyBars*features + MetaDescWidth(), so //--- every feedForward on a meta net MUST run this between window build and forward. virtual void AppendCandidateFeatures(const int candId) {} //--- Width of that descriptor; 0 for direction models so NetInputWidth() stays byte-identical. virtual int MetaDescWidth(void) const { return 0; } //--- The one true input width every feedForward guard compares against. int NetInputWidth(void) const { return (int)m_historyBars * m_neuronsCount + MetaDescWidth(); } //--- P(win) from the 2-output head's raw activations in TempData (after Net.getResults) - the //--- 2-class softmax collapses to a logistic over the logit difference. Same CLASS_LOGIT_SCALE the //--- training gradient applies, so the probability is the one the loss was optimizing. -1 = no data. double MetaWinProbability(void) { if(TempData.Total() < 2) return -1.0; double z = CLASS_LOGIT_SCALE * (TempData.At(0) - TempData.At(1)); return 1.0 / (1.0 + MathExp(-z)); } //--- Triple-barrier outcome of the candidate's own side at its fire bar - the meta LABEL. Reads the //--- side-conditional win caches the label prebuild already computes for every bar; loss AND //--- timeout are both 0, matching the design ("win=1 / loss-or-timeout=0"). bool MetaCandidateWon(const int candId, const int barIdx) { if(barIdx < 0 || barIdx >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[barIdx]) return false; //--- Three-state on purpose: an id that is not a candidate is not a losing LONG, and the //--- old `side > 0` test answered exactly that for one. if(!m_metaCands.Contains(candId)) return false; if(m_metaCands.IsLong(candId)) return (barIdx < ArraySize(m_winLongCache)) ? m_winLongCache[barIdx] : false; return (barIdx < ArraySize(m_winShortCache)) ? m_winShortCache[barIdx] : false; } //--- First candidate id at a bar (-1 none) / next in the same-bar chain. int MetaCandFirst(const int barIdx) const { return m_metaCands.FirstAt(barIdx); } int MetaCandNext(const int candId) const { return m_metaCands.Next(candId); } //--- 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. int ConvReceptiveFieldBars(void) const; int ConvFirstStagePositions(void) const; bool HasSecondConvStage(void) const; int ConvOutputPositions(void) const; int ConvOutputWidth(void) const; //--- Actual input width the LSTM block sees, which is NOT always the flattened input. int LstmFanIn(void) const; //--- " | conv 21->8 x20 bars | lstm 160->32" for the startup config line; "" when neither applies. string FrontEndConfigSummary(void) const; //--- 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. bool AddBatchNormStage(CArrayObj *topology, int 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). int ComputeFirstLayerWidth(void) const; //--- 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. double EstimatedInSampleBars(void) const; //--- The same figure BEFORE the overlap deflation, for reports that want to show both. Never size //--- anything from this one - that was the bug. double EstimatedInSampleBarsRaw(void) const; //--- Width of the vector the first dense layer actually sees: the front-end stage's output where //--- one exists, the flattened window otherwise. int FirstLayerFanIn(void) const; //--- 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). int ComputeConvFilterCount(void) const; int ComputeLstmHiddenSize(void) const; //--- 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. int ComputeHiddenLayerCount(void) const; //--- 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); //--- 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: AIBase\Topology.mqh. string BuildModelFingerprint(void); //--- Exclusive per-config claim, so two charts can never train into one set of model files. bool AcquireConfigLock(void); void ReleaseConfigLock(void); //--- Chart arrows, persistence, the status panel and the HUD line all live in CChartUI now - see //--- Expert\Chart\ChartUI.mqh. These stay as thin forwards: DisplayHudLine is a virtual override //--- (dispatched from Warrior_EA.mq5), the rest are called by name from Training.mqh and this //--- file's own live-tick path, and 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; // 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; //--- 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. //--- EXCURSION-SIZE HEAD - see Expert\AIBase\Excursion.mqh. Predicts how FAR price travels, never //--- which way; Stage 1 measures whether it beats a constant ATR multiple and places no orders. bool ExcursionBuildTopology(CArrayObj &topology); bool ExcursionEnsureHead(void); bool ExcursionTargets(int idx); void ExcursionTrainStep(int idx); void ExcursionScoreStep(int idx); void ExcursionTrailPush(void); void ExcursionResetEraScores(void); double ExcursionQuantile(bool upward, double tau); void ExcursionReport(void); void ResetDirConfHistogram(void); void AccumulateDirConfSample(double margin, bool wasCorrect, bool isPrimaryBar); void FitDirConfThreshold(void); //--- 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 (int)MathMax(m_barrierHorizonBars, 1); } 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); bool LoadModelStats(string fileName, bool 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); //--- 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(); //--- RESEARCH ONLY - gated at runtime by m_exportFeaturesOnly (the ExportFeaturesOnly input), not //--- by a compile flag. Dumps exactly what the network sees - one row per bar: index, time, OHLC, //--- ATR, then the m_neuronsCount feature values - to a CSV under Common\Files\Warrior_EA\Research\. void ExportFeatureMatrix(void); //--- Raw OHLCV for a grid of symbols/timeframes - see the definition for why the grid is worth more //--- than the engineered features on their own. void ExportRawRates(void); bool BufferTempData(int idx); //--- Assembles the full m_historyBars-wide input window ending AT bar r into TempData, OLDEST //--- BAR FIRST. See the definition comment in AIBase\Features.mqh for the measurement behind //--- that. bool BuildFeatureWindow(int 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) - see its implementation comment and the //--- ONLINE_LEARN_* tunables. No-op in the tester/optimizer (m_inferenceOnly) and while training //--- is active. void OnlineLearnStep(void); //--- Alpha-balanced focal sample weight (Lin et al. 2017 eq. 5) for ONE streamed bar - see the //--- ONLINE_LEARN_* block's CLASS IMBALANCE comment for the derivation. Returns 1.0 for the //--- regression head (no class structure). double OnlineSampleWeight(ENUM_SIGNAL trueSignal, double pBuy, double pSell, double pNeutral); //--- lazily bootstraps m_shadowNet if it's still NULL: tries loading a persisted shadow file //--- first (continuity across EA restarts), falling back to cloning Net's current weights (via //--- the same Save()/Load() pattern StartOosContinualSimulation() uses for m_simOosNet) if no //--- compatible shadow file exists yet. void EnsureShadowNet(void); //--- persists m_shadowNet alongside every Net.Save() call, using the same run metadata (error/ //--- undefine/forecast/era/trainingComplete/indicator params) the caller already computed for //--- Net.Save() itself - see m_shadowNet's declaration comment. void SaveShadowNet(const double &indicatorParams[]); //--- method of initialization of the indicators bool InitOpen(CIndicators *indicators); bool InitClose(CIndicators *indicators); bool InitHigh(CIndicators *indicators); bool InitLow(CIndicators *indicators); bool InitVolumes(CIndicators *indicators); bool InitTime(CIndicators *indicators); //--- addToCollection=false is used by ReInitADIndicators() 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); bool InitRSI(CIndicators *indicators, bool addToCollection = true); bool InitMACDFeature(CIndicators *indicators, bool addToCollection = true); bool InitIchimoku(CIndicators *indicators, bool addToCollection = true); bool InitADCumulativeDelta(CIndicators *indicators, bool addToCollection = true); bool InitADShorteningOfThrust(CIndicators *indicators, bool addToCollection = true); bool InitADWyckoffEventStream(CIndicators *indicators, bool addToCollection = true); bool InitADWyckoffFailedStructure(CIndicators *indicators, bool addToCollection = true); bool InitADWyckoffSignificantBarInversion(CIndicators *indicators, bool addToCollection = true); bool InitADZigZag(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); //--- 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); //--- Retry helpers 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 CopyFileWithRetry(string srcFileName, string dstFileName); bool CopySharedFile(string srcFileName, string dstFileName, bool quiet); bool LoadNetWithRetry(double &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; //--- RSI is already a 0-100 oscillator, so the only transform needed is /100 to match every //--- other feature's roughly [-1,1]/[0,1] scale - no ATR or distance normalization applies. bool m_useRSI; //--- MACD as 3 ATR-normalized values (main line, signal line, histogram) - see //--- BufferTempDataCompute()'s m_useMACD block. ATR-normalized rather than left raw because the //--- MACD lines live in price units, exactly like the MA feature. bool m_useMACD; //--- Ichimoku as 8 values - see BufferTempDataCompute()'s m_useIchimoku block for each. The //--- feature block applies the +Kijun offset and never calls ChinkouSpan(); //--- Signals\SignalIchimoku.mqh's class comment documents the buffer convention in full, and the //--- same reasoning governs both. bool m_useIchimoku; //--- 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_ADZigZag 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); //--- 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); //--- 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; int m_spreadSeries[]; int m_spreadSeriesBars; //--- Newest bar the copy was anchored to. Same invalidation key the label/feature bar caches use //--- (see EnsureBarCachesCapacity) and the same failure the zero-direction hunt traced. datetime m_spreadSeriesAnchor; datetime m_crossAssetAnchor; bool EnsureSpreadSeries(int bars); bool m_useADCumulativeDelta; bool m_useADShorteningOfThrust; bool m_useADWyckoffEventStream; bool m_useADWyckoffFailedStructure; bool m_useADWyckoffSignificantBarInversion; 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; } virtual double AIConfidence(void) override { return CalibratedConfidenceMagnitude(); } // Signed for direction-aware use (AI-driven early exit): 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. 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[]); //--- The Intelligent-direction drift verdict, rescanned from the label cache - body and the //--- full statistics note in AIBase\Labels.mqh. Runs at prebuild and at every era end. void RefreshDriftVerdict(void); //--- 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; } } //--- Which target this model trains toward: true = the meta head (trade-quality over fired //--- candidates), false = a per-bar direction model. Reads the constructor-set target; nothing //--- can flip it. bool IsMetaTarget(void) const { return m_trainTarget == 1; } //--- A meta head is a GATE: its Long/ShortCondition are structurally 0 and its verdict reaches //--- the pipeline through its CMetaGate role, never through the vote. AddFilter() keeps it out of //--- the voting list on this answer - see CExpertSignalCustom::IsVotingSignal. virtual bool IsVotingSignal(void) const override { return !IsMetaTarget(); } //--- 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; } //--- 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; return ModuleWeight(); } //--- 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; } //--- BREAK-EVEN THAT KNOWS ABOUT THE HORIZON. CostAdjustedBreakEvenPct is risk/(risk+reward): //--- the win rate a trade needs when it is CERTAIN to end at one barrier or the other. double EmpiricalBreakEvenPct(void) { double geom = CostAdjustedBreakEvenPct(); double t = m_lastTimeoutShare; double m = m_lastTimeoutMeanR; //--- Prefer the era in flight once its own replay has completed; otherwise the latched one. if(m_simTrades > 0) { t = (double)m_simTimeouts / m_simTrades; m = (m_simTimeouts > 0) ? m_simTimeoutRSum / m_simTimeouts : 0.0; } if(t < 0.0) return geom; double adj = geom * (1.0 - t * (1.0 + m)); //--- A timeout mean below -1 R is not reachable (the stop would have taken it first), so this //--- cannot go negative from honest inputs. Clamped anyway: a break-even at or below zero would //--- read as "any win rate pays", which is never a true statement about a trade. return (adj > 0.0 && adj <= geom) ? adj : geom; } //--- "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); } //--- This model's cached decision for bar `idx`, already converted to the signed vote it would //--- have cast. Body in Expert\Chart\ChartUI.mqh. virtual string DisplayHudLine(void) override { return m_chartUI.DisplayHudLine(); } //--- 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; //--- The meta head has no directional prospect - reporting one (even weight-only) would //--- count it as a neutral voter in the prospective readout and dilute its denominator, the //--- display twin of the VoteCapableWeight exclusion above. Its HUD line shows the gate. if(IsMetaTarget()) return false; //--- 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; } //--- 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. void MinDirectionalRecall(int value) { m_minDirectionalRecallPct = value; } //--- 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 LogitAdjustTau(double value) { m_logitAdjustTau = MathMax(0.0, value); } void FreezePriorCalibration(bool value) { m_freezePriorCalibration = value; } void SignalClusterWindow(int value) { m_signalClusterWindow = value; } int SignalClusterWindow(void) const { return m_signalClusterWindow; } void SwingConfirmationBars(int value) { m_swingConfirmationBars = value; } //--- Called from ConfigureAISignal when the TrainingTarget input selects the fractal label. Guarded //--- so CSignalMETA (whose constructor already claimed target 1) can never be flipped: the meta //--- head's 2-output topology and candidate pipeline are incompatible with a per-bar 3-class label. void TrainTargetFractal(void) { if(m_trainTarget == 0) m_trainTarget = 2; } //--- 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); } //--- 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 winLong, const bool winShort, 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_ensVoteWinLong, cap); ArrayResize(g_ensVoteWinShort, cap); ArrayResize(g_ensVoteDirLabel, 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; //--- outcomes and label come from the shared label cache, so they are identical across //--- members - whichever member reaches the bar first writes them g_ensVoteWinLong[row] = winLong; g_ensVoteWinShort[row] = winShort; g_ensVoteDirLabel[row] = dirLabel; } 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; 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) { SetStatusLabel(text); 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_enableOnlineLearning = value; } //--- Exit policy, pushed in from Warrior_EA.mq5 so the gate grades the same rule the live path //--- runs. voteThreshold is Signal_ThresholdClose UNSCALED, on the same 0-100 confidence scale //--- as the live close threshold (>100 disables it by arithmetic, exactly as live does). void ExitPolicy(double voteThreshold, bool holdToBarrier) { m_exitVoteThreshold = (voteThreshold > 100.0) ? 0.0 : voteThreshold; m_exitHoldToBarrier = holdToBarrier; } 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; } void UseTime(bool value) { m_useTime = value; } void UseATR(bool value) { m_useATR = value; } void UseMA(bool value) { m_useMA = value; } void UseRSI(bool value) { m_useRSI = value; } void UseMACD(bool value) { m_useMACD = value; } void UseIchimoku(bool value) { m_useIchimoku = value; } void UseSwingContext(bool value) { m_useSwingContext = value; } void UseNews(bool value) { m_useNews = value; } void NewsFeatureWindowMinutes(int value) { m_newsFeatureWindowMinutes = value; } void UseCrossAsset(bool value) { m_useCrossAsset = value; } void UseSpreadFeature(bool value) { m_useSpreadFeature = value; } void UseADCumulativeDelta(bool value) { m_useADCumulativeDelta = value; } void UseADShorteningOfThrust(bool value) { m_useADShorteningOfThrust = value; } void UseADWyckoffEventStream(bool value) { m_useADWyckoffEventStream = value; } void UseADWyckoffFailedStructure(bool value) { m_useADWyckoffFailedStructure = value; } void UseADWyckoffSignificantBarInversion(bool value) { m_useADWyckoffSignificantBarInversion = value; } void AutoTuneIndicators(bool value) { m_autoTuneIndicators = value; } void ExportFeaturesOnly(bool value) { m_exportFeaturesOnly = value; } void UseAltData(bool value) { m_altDataEnabled = value; } //--- 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 DataHorizonBars(void) const { return m_barrierHorizonBars; } int DataPurgeBars(void) { 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]); } bool DataOutcome(const int bar, bool &wonLong, bool &wonShort) const { wonLong = wonShort = false; if(bar < 0 || bar >= ArraySize(m_winLongCache) || bar >= ArraySize(m_winShortCache)) return false; wonLong = m_winLongCache[bar]; wonShort = m_winShortCache[bar]; return true; } bool DataExcursion(const int bar, double &up, double &down) const { up = down = 0.0; if(bar < 0 || bar >= ArraySize(m_excUpCache) || bar >= ArraySize(m_excDownCache)) return false; up = m_excUpCache[bar]; down = m_excDownCache[bar]; return (MathIsValidNumber(up) && MathIsValidNumber(down)); } //--- -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; } 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); } //--- 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 = (CheckPointer(m_shadowNet) != POINTER_INVALID) ? m_shadowNet : Net; 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); } 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 &buyPredictedWins, int &sellPredictedWins) const { buyPredicted = m_oos.buyPredicted; sellPredicted = m_oos.sellPredicted; buyPredictedWins = m_oos.buyPredictedWins; sellPredictedWins = m_oos.sellPredictedWins; } 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; } bool ChartMetaHasScore(void) const { return m_metaTelemetry.HasScore(); } double ChartMetaLastP(void) const { return m_metaTelemetry.lastP; } double ChartMetaLastBe(void) const { return m_metaTelemetry.lastBe; } int ChartMetaApproved(void) const { return m_metaTelemetry.approved; } int ChartMetaVetoed(void) const { return m_metaTelemetry.vetoed; } double ChartDispProb(const int i) const { return (i >= 0 && i < 3) ? m_dispProbs[i] : 0.0; } double ChartDispSignal(void) const { return m_dispSignal; } //--- 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); } void ChartBarrierMultiples(double &slMult, double &tpMult) { BarrierMultiples(slMult, tpMult); } bool ChartDisplayInference(void) { return DisplayInference(); } bool ChartMetaGateArmedNow(void) { return MetaGateArmedNow(); } //--- 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_simOosRunActive); 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; if(m_simOosRunActive) { delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; } //--- 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) + " (balanced accuracy " + (m_bestBalancedOos < 0 ? "n/a" : DoubleToString(m_bestBalancedOos, 1) + "%") + ", blended OOS " + DoubleToString(dOosForecast, 1) + "%) - training stopped, now running live inference" + (m_enableOnlineLearning ? " 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; } double currentIndicatorParams[]; m_indicatorTuner.Flatten(currentIndicatorParams); bool ok = Net.Save(m_activeFileName + ".nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, currentIndicatorParams); //--- 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 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; 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; //--- A reset declares there are no fitted weights left to protect, so the geometry must //--- re-derive. m_geometryDerivePasses is the one that blocks it: LoadAndCompare pins it to //--- BARRIER_DERIVE_MAX_PASSES so an EXISTING model can never move its target, which is right //--- for a load and wrong here. Miss this and every reset relabels under the OLD pair. m_geometryDerived = false; m_geometryAdopted = false; m_geometryCfgSaved = false; // let the re-derived pair pin itself to the fresh .cfg m_geometryDerivePasses = 0; // the fixed-point iteration runs again from scratch m_derivedSlMult = 0.0; m_derivedTpMult = 0.0; //--- These feed the derivation, so they re-measure with it - a stale horizon would size the new //--- target from travel measured under the old one. m_barrierHorizonResolved = false; m_barrierHorizonBars = BARRIER_HORIZON_FALLBACK; m_barrierHorizonLegStarved = false; m_barrierHorizonClamped = false; m_barrierFallbackWarned = false; m_swingMedianLegAtr = 0.0; //--- Back to 0, not merely left alone: DeriveBarrierGeometry's cost filter is deliberately inert //--- on pass 1 (m_spreadAtr <= 0), and an inherited value makes it reject rungs on a cost it was //--- designed not to know yet - a different ladder than a genuinely new model would walk. m_spreadAtr = 0.0; m_barrierScanSlMult = 0.0; m_barrierScanTpMult = 0.0; m_barrierScanLiveLabels = false; m_barrierScanTimeouts = 0; //--- LoadAndCompare overwrites these in place when it adopts a trained pair, keeping no copy of //--- what the user configured; leaving them adopted seeds the fresh derivation from the old pair. m_sl_mode = SL_Mode; m_tp_mode = TP_Mode; if(m_simOosRunActive) { delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; } //--- 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); 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" #include "AIBase\OnlineLearning.mqh" #include "AIBase\AutoTune.mqh" #include "AIBase\FeatureScreen.mqh" #include "AIBase\Inference.mqh" #include "AIBase\Persistence.mqh" #include "AIBase\Features.mqh" #include "AIBase\Excursion.mqh" #include "Training\AIBaseTrainingDataImpl.mqh" #include "Chart\AIBaseChartViewImpl.mqh" //+------------------------------------------------------------------+