//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Indicator creation and the per-bar input feature vector. | //| | //| PARTIAL IMPLEMENTATION FILE - not standalone. | //| This holds CExpertSignalAIBase method BODIES only. The class | //| declaration lives in Expert\ExpertSignalAIBase.mqh, which | //| #includes this file at the bottom, after the declaration. Do not | //| include it anywhere else and do not compile it on its own. | //| | //| Split out purely to make the 8216-line original navigable; the | //| code inside was moved verbatim, not rewritten. | //+------------------------------------------------------------------+ #ifndef WARRIOR_AIBASE_FEATURES_MQH #define WARRIOR_AIBASE_FEATURES_MQH //--- Plausibility ceiling for any single input value, enforced once over the whole bar at the end of //--- BufferTempDataCompute(). Deliberately far above every clamp used inside that function (the widest //--- is +/-10) - this is not a normalization knob, it is the "no legitimate feature looks like this" //--- line. See the sanitize loop at the end of BufferTempDataCompute() for what it protects. #define FEATURE_ABS_MAX 1.0e4 //+------------------------------------------------------------------+ //| Rebuilds only the enabled AD* CiCustom handles in place, so a new | //| trial's member-struct param values take effect. Re-Create()-ing | //| the existing CiCustom object (rather than removing/re-adding it | //| to indicators) avoids adding the same pointer into the CIndicators| //| collection twice, which would risk it being deleted twice on | //| teardown - MQL5's CIndicators has no documented single-item | //| remove. | //| | //| This used to end "...and CiCustom.Create() already releases its | //| old handle." IT DOES NOT, and that sentence cost two models. See | //| the handle-release block in the definition below. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| See the declaration. Minimum over the enabled tunable indicators, | //| because the feature vector is only as ready as its least-ready | //| component; -1 when nothing tunable is switched on. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::TunableBarsCalculated(int &enabled) { enabled = 0; int worst = INT_MAX; if(m_useMA) { enabled++; worst = (int)MathMin(worst, m_MA.BarsCalculated()); } if(m_useRSI) { enabled++; worst = (int)MathMin(worst, m_RSI.BarsCalculated()); } if(m_useMACD) { enabled++; worst = (int)MathMin(worst, m_MACDFeature.BarsCalculated()); } if(m_useIchimoku) { enabled++; worst = (int)MathMin(worst, m_Ichimoku.BarsCalculated()); } if(m_useADCumulativeDelta) { enabled++; worst = (int)MathMin(worst, m_ADCumulativeDelta.BarsCalculated()); } if(m_useADShorteningOfThrust) { enabled++; worst = (int)MathMin(worst, m_ADShorteningOfThrust.BarsCalculated()); } if(m_useADWyckoffEventStream) { enabled++; worst = (int)MathMin(worst, m_ADWyckoffEventStream.BarsCalculated()); } if(m_useADWyckoffFailedStructure) { enabled++; worst = (int)MathMin(worst, m_ADWyckoffFailedStructure.BarsCalculated()); } if(m_useADWyckoffSignificantBarInversion) { enabled++; worst = (int)MathMin(worst, m_ADWyckoffSignificantBarInversion.BarsCalculated()); } return (worst == INT_MAX) ? -1 : worst; } //+------------------------------------------------------------------+ //| Back-compatible form for the callers that only want the number. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::TunableBarsCalculated(void) { int enabled = 0; return TunableBarsCalculated(enabled); } //+------------------------------------------------------------------+ //| See the declaration. THE one place that decides how much history | //| may be asked of the indicators; every ResizeBuffers() call site | //| goes through it. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::ServableBars(int want, string context) { if(want <= 0) return want; int enabled = 0; int servable = TunableBarsCalculated(enabled); //--- THE BLIND SPOT THAT COST 2026-08-17 (fixed the same day, after the fact). This used to read //--- `if(servable <= 0 || servable >= want) return want;` - one branch covering three completely //--- different states, and returning silently from all of them: //--- enabled == 0 -> no tunable indicator is on, so there is genuinely no cap. Fine. //--- enabled > 0, servable == -1 -> a handle answered INVALID. CopyBuffer will fail at every index. //--- enabled > 0, servable == 0 -> a handle is created but has calculated nothing. //--- The last two are the exact state the whole depth investigation was looking for, and the gate //--- built to find it returned `want` without printing a character. That is why 27 MB of journal //--- from a build that HAD this instrumentation contained zero depth lines while two charts sat at //--- ok=0/failed=50163 for 38 minutes: the instrument's one silent path was the state it was hunting. //--- The dead case is now REPORTED and REPAIRED. This function still answers `want` either way, so //--- its own contract ("want, clamped to what the indicators can serve") is unchanged for live //--- inference and online learning, which have their own refusal paths. Only SettledBars() - the //--- training sweep's entry point, the one caller that can afford to wait - converts the same state //--- into a HOLD. See the note at its own dead-handle branch for why the two deliberately differ. if(enabled == 0 || servable >= want) { //--- Cleared on the healthy path too, not only on the clamp path below: a handle that recovers //--- all the way to full depth would otherwise leave the latch set and a LATER outage would be //--- swallowed - which is the failure mode this whole function is being fixed for. m_indicatorDepthDeadWarned = false; return want; } if(servable <= 0) { //--- REPORT FIRST, THEN REPAIR - in that order, so the depths in this line are the ones that //--- caused it. Repairing first would print the freshly-created handle's numbers next to a //--- message about a dead one, which is exactly the kind of self-contradicting log line that //--- makes the next reader distrust the whole file. if(!m_indicatorDepthDeadWarned) { m_indicatorDepthDeadWarned = true; PrintFormat("%s: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - %d tunable indicator(s) enabled" " and the least-ready answers BarsCalculated()=%d while %s asked for %d. -1 means an" " INVALID HANDLE, 0 means created-but-never-calculated; either way CopyBuffer fails" " at EVERY index, the buffer holds nothing, and every feature block that reads it" " rejects every bar. This is NOT the depth cap below (that one clamps and trains on" " what is servable) - there is nothing to clamp to. Per-indicator depth:%s", ID, enabled, servable, context, want, IndicatorDepthReport()); } //--- A dead handle answers EMPTY_VALUE at every index, so a 50k-bar pass over it is 50k guaranteed //--- rejections followed by a discarded era, forever - the exact loop that froze USDJPY and //--- XAUUSD. Rate-limited inside, and it only touches handles reporting < 0, so a merely COLD //--- indicator (valid handle, 0 bars calculated) is left alone to warm up normally. RepairDeadIndicatorHandles(); return want; } m_indicatorDepthDeadWarned = false; //--- -(m_historyBars + 2): the deepest window slot reads (r + m_historyBars - 1), and the MA block //--- one further back again for its bar-over-bar change, so the last usable anchor sits that far //--- inside the buffer. Without the margin the clamp would hand back a depth whose own deepest //--- window still reads off the end - the exact failure it exists to prevent. int capped = servable - ((int)m_historyBars + 2); if(capped < 0) capped = 0; //--- Depends only on `servable` and m_historyBars, never on `want`, so it is stable across call //--- sites and this logs once per real change rather than once per era per context. if(m_indicatorDepthCapBars != capped) { m_indicatorDepthCapBars = capped; PrintFormat("%s: indicator history CAPPED to %d bars (%s asked for %d) - the price series has" " that much, but the least-ready tunable indicator has only calculated %d. Past what" " an indicator has calculated CopyBuffer does not short-read, it FAILS, so the buffer" " holds NOTHING and EVERY index reads EMPTY_VALUE - indistinguishable from a cold" " indicator. Per-indicator depth:%s", ID, capped, context, want, servable, IndicatorDepthReport()); } return capped; } //+------------------------------------------------------------------+ //| See the declaration. ServableBars() with the WAIT in front of it. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::SettledBars(int want, string context) { if(want <= 0) return want; int enabled = 0; int servable = TunableBarsCalculated(enabled); //--- Same three-states-one-branch defect ServableBars() carried (see the long note there): `servable //--- < 0` was read as "nothing tunable is on", but it is ALSO what a dead handle answers. Settling a //--- dead handle is pointless - it will never climb - so this still falls straight through, but only //--- ServableBars() gets to decide that now, and it reports the case on the way past. if(enabled == 0 || servable >= want) { m_depthSettleStart = 0; m_depthProbeStable = 0; m_depthProbeLast = 0; return want; } if(servable <= 0) { m_depthSettleStart = 0; m_depthProbeStable = 0; m_depthProbeLast = 0; //--- Routed through ServableBars() rather than answering here, and that detour is the whole point: //--- the training sweep - the ONLY caller that reaches the dead-handle state in practice - calls //--- SettledBars, not ServableBars. Returning directly is what kept the report (and now the //--- repair) unreachable from the one path that needed them. ServableBars(want, context); //--- 0 = HOLD, and this is the one place the two functions deliberately disagree. ServableBars //--- must answer `want` for its own callers (live inference and online learning have their own //--- refusal paths and a 0 there would read as "no history at all"), but the training sweep can //--- afford to wait and must: nothing is readable at any index right now, so a full-history pass //--- would reject every bar, discard the era and start over - the exact 38-minute loop this is //--- being fixed for. Whether the handle was just recreated (cold, will climb) or is still dead //--- (repair failed), holding is right; Train() reports the hold every minute and the era-barrier //--- liveness escape releases the rest of the ensemble if it never resolves. return 0; } uint now = GetTickCount(); //--- First shortfall: start the clock and let the priming request above do its work. Deliberately //--- no sweep this call - a 50k-bar feature sweep is exactly what starves the indicator threads we //--- are waiting on, which is how the old loop sustained itself for 40 minutes at a time. if(m_depthSettleStart == 0) { m_depthSettleStart = now; m_depthProbeTick = now; m_depthProbeLast = servable; m_depthProbeStable = 0; PrintFormat("%s: PRIMING indicator history for the %s - %d of %d bars calculated so far. Holding" " the sweep until the count stops rising (probe every %ds, needs %d steady probes," " gives up after %ds and uses whatever is there). Per-indicator depth:%s", ID, context, servable, want, DEPTH_SETTLE_PROBE_MS / 1000, DEPTH_SETTLE_STABLE_PROBES, DEPTH_SETTLE_TIMEOUT_MS / 1000, IndicatorDepthReport()); return 0; } //--- Unsigned subtraction, so this is correct across GetTickCount()'s 49-day wrap (same idiom as //--- m_coldSweepTick's backoff). if(now - m_depthProbeTick < DEPTH_SETTLE_PROBE_MS) return 0; m_depthProbeTick = now; if(servable != m_depthProbeLast) { //--- STILL MOVING. Growing is the terminal working through the history; shrinking happens when a //--- handle is rebuilt under us and starts over. Either way it is not settled, so the streak //--- restarts rather than counting a change as a steady observation. PrintFormat("%s: priming %s - %d of %d bars (was %d), still moving", ID, context, servable, want, m_depthProbeLast); m_depthProbeLast = servable; m_depthProbeStable = 0; return 0; } m_depthProbeStable++; bool steady = (m_depthProbeStable >= DEPTH_SETTLE_STABLE_PROBES); bool expired = ((now - m_depthSettleStart) >= DEPTH_SETTLE_TIMEOUT_MS); if(!steady && !expired) return 0; //--- Settled (or waited long enough) BELOW what was asked. This is the real depth, not a snapshot of //--- a value still climbing, so it is now safe to clamp to it and get on with training. PrintFormat("%s: priming %s DONE - depth settled at %d of %d bars after %ds%s. Training proceeds on" " the %d bars the indicators can actually serve.", ID, context, servable, want, (int)((now - m_depthSettleStart) / 1000), expired && !steady ? " (gave up waiting - it never went steady)" : "", servable); m_depthSettleStart = 0; m_depthProbeStable = 0; m_depthProbeLast = 0; return ServableBars(want, context); } //+------------------------------------------------------------------+ //| See the declaration. What this configuration would have to FIRE | //| before any edge of a given size becomes certifiable. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ReportDetectability(int oosBars) { if(m_detectabilityReported || oosBars <= 0) return; m_detectabilityReported = true; double p = CostAdjustedBreakEvenPct() / 100.0; if(p <= 0.0 || p >= 1.0) return; double L = MeanLabelLifespan(); //--- Invert the deploy gate. It passes when edge >= z * sqrt(p(1-p)/n_eff), so for a hypothesised //--- true edge d it needs n_eff >= z^2 p(1-p) / d^2 independent calls - and since overlapping //--- labels are worth ~1/L each, that is L times as many RAW calls. Everything on the right-hand //--- side is a property of the CONFIGURATION (geometry via p, horizon via L, window via oosBars), //--- not of the model, which is the whole point: no amount of training moves it. string ladder = ""; double edges[3] = {2.0, 5.0, 10.0}; for(int i = 0; i < 3; i++) { double d = edges[i] / 100.0; double needEff = (EDGE_MIN_SIGMAS * EDGE_MIN_SIGMAS) * p * (1.0 - p) / (d * d); double needRaw = needEff * L; double needCoverage = 100.0 * needRaw / (double)oosBars; ladder += StringFormat(" %+.0fpp:%.0f indep=%.0f calls=%.0f%% of window%s |", edges[i], needEff, needRaw, needCoverage, needCoverage > 100.0 ? " IMPOSSIBLE" : ""); } PrintFormat("%s: DETECTABILITY of this configuration (break-even %.1f%%, mean label lifespan %.1f" " bars, OOS window %d bars) - to certify an edge of X the gate needs:%s" " Read it as a budget, not a target: these are properties of the GEOMETRY, the HORIZON" " and the WINDOW, so a better model cannot change any of them. Where a rung says" " IMPOSSIBLE, no win rate this model could ever produce would clear the deploy bar on" " this window - the answer there is more instruments, a lower timeframe or a narrower" " barrier, never more eras. Coverage is also not free in the other direction: firing on" " more bars buys independent calls at the cost of precision, so the reachable band is" " bounded at both ends.", ID, 100.0 * p, L, oosBars, ladder); } //+------------------------------------------------------------------+ //| See the declaration. The per-class COLLAPSE floor, derived rather | //| than configured. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::CollapseRecallFloorPct(int classTrueCount) { //--- Chance recall is 1/K for K classes and does not depend on the class priors: a zero-skill model //--- that emits class c with probability q gets recall q on EVERY true class, and the uniform //--- zero-skill model has q = 1/K. That, not a configured constant, is the reference this floor is //--- measured against. double chance = 100.0 / 3.0; double effN = EffectiveSampleSize((double)classTrueCount); if(effN <= 0.0) return (double)m_minDirectionalRecallPct; double q = 1.0 / 3.0; double se = 100.0 * MathSqrt(q * (1.0 - q) / effN); double floorPct = chance - EDGE_MIN_SIGMAS * se; //--- Never negative, and never so high it becomes the unreachable bar this replaced. if(floorPct < 0.0) floorPct = 0.0; return floorPct; } //+------------------------------------------------------------------+ //| RE-CREATE any enabled tunable indicator whose handle has gone | //| INVALID underneath us. See the declaration for the evidence. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::RepairDeadIndicatorHandles(void) { if(m_indicatorsPtr == NULL) return false; uint now = GetTickCount(); //--- Cooldown, because every consumer of ServableBars() can reach this - the training sweep, live //--- inference on every tick, online learning - and a repair storm against a terminal that is //--- genuinely refusing to create the indicator would be worse than the outage it is fixing. if(m_handleRepairTick != 0 && now - m_handleRepairTick < HANDLE_REPAIR_COOLDOWN_MS) return false; m_handleRepairTick = now; //--- NOT released first, deliberately. BarsCalculated() answering -1 means the terminal no longer //--- knows this handle, so there is nothing to give back - and MT5 recycles handle VALUES, so //--- releasing a stale one risks decrementing whatever now owns that number. Re-Create only. //--- addToCollection is false throughout: the CiCustom/CiMA objects are already in m_indicatorsPtr //--- from InitIndicators(), and adding them twice would let the collection delete them twice. int repaired = 0; string before = IndicatorDepthReport(); if(m_useMA && m_MA.BarsCalculated() < 0) repaired += (InitMA(m_indicatorsPtr, false) ? 1 : 0); if(m_useRSI && m_RSI.BarsCalculated() < 0) repaired += (InitRSI(m_indicatorsPtr, false) ? 1 : 0); if(m_useMACD && m_MACDFeature.BarsCalculated() < 0) repaired += (InitMACDFeature(m_indicatorsPtr, false) ? 1 : 0); if(m_useIchimoku && m_Ichimoku.BarsCalculated() < 0) repaired += (InitIchimoku(m_indicatorsPtr, false) ? 1 : 0); if(m_useADCumulativeDelta && m_ADCumulativeDelta.BarsCalculated() < 0) repaired += (InitADCumulativeDelta(m_indicatorsPtr, false) ? 1 : 0); if(m_useADShorteningOfThrust && m_ADShorteningOfThrust.BarsCalculated() < 0) repaired += (InitADShorteningOfThrust(m_indicatorsPtr, false) ? 1 : 0); if(m_useADWyckoffEventStream && m_ADWyckoffEventStream.BarsCalculated() < 0) repaired += (InitADWyckoffEventStream(m_indicatorsPtr, false) ? 1 : 0); if(m_useADWyckoffFailedStructure && m_ADWyckoffFailedStructure.BarsCalculated() < 0) repaired += (InitADWyckoffFailedStructure(m_indicatorsPtr, false) ? 1 : 0); if(m_useADWyckoffSignificantBarInversion && m_ADWyckoffSignificantBarInversion.BarsCalculated() < 0) repaired += (InitADWyckoffSignificantBarInversion(m_indicatorsPtr, false) ? 1 : 0); if(repaired == 0) return false; //--- Every cached feature row was computed against the handle that just got replaced. The values //--- themselves are identical (same params, same series) so this is belt-and-braces, but a cache //--- keyed to a handle that no longer exists is exactly the kind of thing that survives a fix and //--- resurfaces a week later. ArrayInitialize(m_featureCacheHasValue, false); PrintFormat("%s: RECREATED %d dead indicator handle(s) - the terminal had stopped recognising them," " so CopyBuffer failed at every index and every bar of the sweep was rejected. Depth" " BEFORE:%s | AFTER:%s. A freshly created handle calculates asynchronously, so the next" " few calls may still report a short depth - that is the normal priming path, not this" " fault. If this line repeats on a cycle, something is releasing the handle out from" " under this member and the recreate is only papering over it.", ID, repaired, before, IndicatorDepthReport()); return true; } //+------------------------------------------------------------------+ //| See the declaration. Names WHICH indicator is short, so the next | //| occurrence is read off the log instead of inferred. | //+------------------------------------------------------------------+ string CExpertSignalAIBase::IndicatorDepthReport(void) { string s = StringFormat(" price=%d", Bars(m_symbol.Name(), PERIOD_CURRENT)); //--- HANDLE VALUES, not just depths. A depth of -1 says "this handle is dead"; the handle NUMBER is //--- what says whether it was never created, or was created and later released out from under this //--- member - and on an ensemble chart all four members request identical params, so MT5 hands them //--- the SAME refcounted handle and one member's release is felt by the others. Comparing the number //--- across members' log lines is what distinguishes those cases. Costs nothing to print. if(m_useMA) s += StringFormat(" MA=%d(h%d)", m_MA.BarsCalculated(), m_MA.Handle()); if(m_useRSI) s += StringFormat(" RSI=%d", m_RSI.BarsCalculated()); if(m_useMACD) s += StringFormat(" MACD=%d", m_MACDFeature.BarsCalculated()); if(m_useIchimoku) s += StringFormat(" Ichi=%d", m_Ichimoku.BarsCalculated()); if(m_useADCumulativeDelta) s += StringFormat(" CumDelta=%d", m_ADCumulativeDelta.BarsCalculated()); if(m_useADShorteningOfThrust) s += StringFormat(" SoT=%d", m_ADShorteningOfThrust.BarsCalculated()); if(m_useADWyckoffEventStream) s += StringFormat(" WES=%d", m_ADWyckoffEventStream.BarsCalculated()); if(m_useADWyckoffFailedStructure) s += StringFormat(" WFS=%d", m_ADWyckoffFailedStructure.BarsCalculated()); if(m_useADWyckoffSignificantBarInversion) s += StringFormat(" WSBI=%d", m_ADWyckoffSignificantBarInversion.BarsCalculated()); //--- Not tunable, so absent from TunableBarsCalculated() - but the swing block reads it on every bar //--- and neutral-fills when it is short, which is silent. Worth seeing next to the others. s += StringFormat(" ZigZag=%d ATR=%d", m_ADZigZag.BarsCalculated(), m_ATR.BarsCalculated()); return s; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Adopt a saved indicator-param set, rebuilding handles only on a | //| REAL change. | //| | //| The resume path restores the params a model was trained with and | //| used to call ReInitADIndicators unconditionally. In the common | //| case the saved set is byte-identical to the values the indicators | //| were created with a few hundred milliseconds earlier (the MI | //| tuner usually keeps the configured settings), so the "rebuild" | //| destroyed five working, already-calculating indicator instances | //| to recreate them with the same inputs - at process start, with | //| history still syncing. On a memory-starved box (2026-08-13: | //| 1 GB free of 31) the replacements stayed cold for 6+ minutes and | //| the resumed model could not train a single era. A no-change adopt | //| now only aligns the tuner state and leaves the live instances | //| alone. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::AdoptIndicatorParams(const double &loaded[], CIndicators *indicators) { double current[]; m_indicatorTuner.Flatten(current); bool changed = (ArraySize(current) != ArraySize(loaded)); if(!changed) for(int k = 0; k < ArraySize(loaded); k++) if(current[k] != loaded[k]) { changed = true; break; } //--- the tuner mirrors the model's params either way - it feeds the .nnw save and the fingerprint m_indicatorTuner.Unflatten(loaded); if(!changed) { PrintVerbose(ID + ": saved indicator params match the live indicators - keeping the existing" " instances (no handle rebuild)."); return true; } Print(ID + ": saved indicator params differ from the live defaults - rebuilding the tunable" " indicator handles to match the model they trained."); return ReInitADIndicators(indicators); } //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ReInitADIndicators(CIndicators *indicators) { bool result = true; //--- RELEASE THE HANDLE EACH Create() IS ABOUT TO REPLACE. Without this every call here leaks one //--- terminal-side indicator instance per enabled indicator, and this function is the tuner's inner //--- loop - AutoTuneIndicators scored 324 candidates on SP500 H1, so ~324 x 6 orphaned instances per //--- model, each holding a full-history buffer set (ADWyckoffEventStream is 14 buffers x ~38k bars x //--- 8 bytes = ~4.3 MB EACH). That is gigabytes, and it is what killed CONV and LSTM on 2026-08-07: //--- 6664 and 2048 "VirtualAlloc failed in large allocator" lines in the terminal journal, then //--- "expert Warrior_EA (SP500,H1) removed", 50ms and 71ms after each finished its sweep and era 0 //--- tried to allocate. HYBRID only survived because those two died first and freed the memory. //--- //--- THE COMMENT THAT USED TO SIT HERE SAID Create() "already releases its old handle". It does not. //--- MQL5's CIndicator::Create (Include\Indicators\Indicator.mqh) is: //--- m_handle = IndicatorCreate(symbol, period, type, num_params, params); //--- - a plain overwrite. Its only success-path IndicatorRelease is in ~CIndicator. Nothing else in //--- this codebase called IndicatorRelease at all. //--- //--- UNCONDITIONAL, not "only when the handle changed". MT5 refcounts indicator instances by //--- (symbol, period, params): re-creating with IDENTICAL params hands back the SAME handle with the //--- count incremented, so skipping the release there would leak a reference just as surely - which is //--- the "23 x WFS(48,3,1.80,1.10)" pattern in the journal, next to the distinct-parameter leaks from //--- the candidate grid. Either way Create() added exactly one reference and we still hold exactly one //--- handle, so exactly one release is owed. int hCD = m_useADCumulativeDelta ? m_ADCumulativeDelta.Handle() : INVALID_HANDLE; int hSOT = m_useADShorteningOfThrust ? m_ADShorteningOfThrust.Handle() : INVALID_HANDLE; int hWES = m_useADWyckoffEventStream ? m_ADWyckoffEventStream.Handle() : INVALID_HANDLE; int hWFS = m_useADWyckoffFailedStructure ? m_ADWyckoffFailedStructure.Handle() : INVALID_HANDLE; int hWSBI = m_useADWyckoffSignificantBarInversion ? m_ADWyckoffSignificantBarInversion.Handle() : INVALID_HANDLE; int hMA = m_useMA ? m_MA.Handle() : INVALID_HANDLE; int hRSI = m_useRSI ? m_RSI.Handle() : INVALID_HANDLE; int hMACD = m_useMACD ? m_MACDFeature.Handle() : INVALID_HANDLE; int hIchi = m_useIchimoku ? m_Ichimoku.Handle() : INVALID_HANDLE; if(m_useADCumulativeDelta) result = InitADCumulativeDelta(indicators, false) && result; if(m_useADShorteningOfThrust) result = InitADShorteningOfThrust(indicators, false) && result; if(m_useADWyckoffEventStream) result = InitADWyckoffEventStream(indicators, false) && result; if(m_useADWyckoffFailedStructure) result = InitADWyckoffFailedStructure(indicators, false) && result; if(m_useADWyckoffSignificantBarInversion) result = InitADWyckoffSignificantBarInversion(indicators, false) && result; if(m_useMA) result = InitMA(indicators, false) && result; if(m_useRSI) result = InitRSI(indicators, false) && result; if(m_useMACD) result = InitMACDFeature(indicators, false) && result; if(m_useIchimoku) result = InitIchimoku(indicators, false) && result; //--- AFTER the re-creates, never before: releasing first can drop the terminal's last reference and //--- make it tear the instance down, so an identical-params Create() would then rebuild it from //--- scratch instead of re-using the live one - turning a refcount bump into a full recalculation over //--- the whole history, 324 times over. Released here, the old instance survives until its //--- replacement exists. if(hCD != INVALID_HANDLE) IndicatorRelease(hCD); if(hSOT != INVALID_HANDLE) IndicatorRelease(hSOT); if(hWES != INVALID_HANDLE) IndicatorRelease(hWES); if(hWFS != INVALID_HANDLE) IndicatorRelease(hWFS); if(hWSBI != INVALID_HANDLE) IndicatorRelease(hWSBI); if(hMA != INVALID_HANDLE) IndicatorRelease(hMA); if(hRSI != INVALID_HANDLE) IndicatorRelease(hRSI); if(hMACD != INVALID_HANDLE) IndicatorRelease(hMACD); if(hIchi != INVALID_HANDLE) IndicatorRelease(hIchi); //--- Indicator params just changed, so every cached feature row is now stale (the feature values //--- depend on these indicators; the LABELS do not - they come from ADZigZag - so the label cache is //--- deliberately left intact and reused). Without this, a tuner candidate would silently train and be //--- scored on the PREVIOUS candidate's features. Cheap: just flags rows for lazy recompute on next read. ArrayInitialize(m_featureCacheHasValue, false); return result; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ResizeBuffers(int barIndex) { // The Ichimoku feature's Chikou term reads m_Close at idx + ichiKijun (see its block in // BufferTempDataCompute() for why that direction, and only that direction, is lookahead-free), which // is further back than any other consumer of the close series reaches. Grow the close buffer to match // when that feature is on, so the oldest requested bars resolve from real data instead of tripping // that block's EMPTY_VALUE guard and being rejected as unusable examples. // CLAMPED TO WHAT EXISTS. CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory only // succeeds when Bars() >= size, and Train() calls this with barIndex == Bars() - so ANY block that // asks for "barIndex + something" fails the whole ResizeBuffers call rather than just its own deep // reads. The MA block proved that on 2026-08-17 with a +1 (see its comment below); the Ichimoku // pair here is the same defect with a much larger constant, latent only because that feature has // been off. The oldest ichiKijun bars then have no cloud, which the block's EMPTY_VALUE guard // already handles as a per-bar rejection - the correct outcome, and cheap next to losing all of it. int maxBars = Bars(m_symbol.Name(), PERIOD_CURRENT); int closeBars = m_useIchimoku ? (int)MathMin(barIndex + m_indicatorTuner.ichiKijun, maxBars) : barIndex; if(!m_Open.BufferResize(barIndex) || !m_Close.BufferResize(closeBars) || !m_High.BufferResize(barIndex) || !m_Low.BufferResize(barIndex)) return false; if(m_useVolumes) { if(!m_Volumes.BufferResize(barIndex)) return false; } // Unconditional - see InitTime()'s call site in InitIndicators() for why m_Time must always be live. if(!m_Time.BufferResize(barIndex)) return false; if(m_useMA) { // NOT barIndex + 1, though the MA block does read GetData(idx) AND GetData(idx + 1) for its // bar-over-bar change. That looks like an off-by-one and is not one: Train() calls this with // barIndex == Bars(), and CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory // only succeeds when Bars() >= size, so asking for one MORE bar than exists fails the whole // ResizeBuffers call. Shipped as a "fix" on 2026-08-17 and it stopped every chart dead: // "failed to get 50180 bars for USDJPY,PERIOD_H4" (Bars() was 50,179) and 33,983 vs 33,982 on // XAUUSD, after which StartLabelCachePrebuild() bailed on the false return - silently, forever, // showing only as "arming the first label-cache prebuild". // The read at the OLDEST bar is SUPPOSED to fail: there is no older bar to difference against. // Rejecting that single bar is correct; buying it costs the entire history. if(!m_MA.BufferResize(barIndex)) return false; } if(m_useRSI) { if(!m_RSI.BufferResize(barIndex)) return false; } if(m_useMACD) { if(!m_MACDFeature.BufferResize(barIndex)) return false; } if(m_useIchimoku) { // + m_indicatorTuner.ichiKijun: the cloud reads reach that many bars FURTHER back than every other // indicator here does (see the m_useIchimoku feature block for why the offset exists), so sizing // this buffer to barIndex alone would leave the oldest requested bars' cloud values unavailable. if(!m_Ichimoku.BufferResize((int)MathMin(barIndex + m_indicatorTuner.ichiKijun, maxBars))) return false; } // Unconditional (not gated by m_useATR): the ATR-normalization in BufferTempData() reads // m_ATR.Main() regardless of whether ATR is enabled as an explicit extra input feature - // m_useATR only controls that feature-count opt-in (see InitIndicators()'s "already init in the // base class" comment), not whether ATR data itself needs to be kept live. if(!m_ATR.BufferResize(barIndex)) return false; // Unconditional, same reasoning as m_ATR above - m_ADZigZag drives the swing-context features AND // ComputeBarrierHorizonBars()'s measurement, not an opt-in feature, so it's never gated by an // m_use* flag. (It was also the training-label source until the 2026-08-01 triple-barrier relabel.) if(!m_ADZigZag.BufferResize(barIndex)) return false; if(m_useADCumulativeDelta) { if(!m_ADCumulativeDelta.BufferResize(barIndex)) return false; } if(m_useADShorteningOfThrust) { if(!m_ADShorteningOfThrust.BufferResize(barIndex)) return false; } if(m_useADWyckoffEventStream) { if(!m_ADWyckoffEventStream.BufferResize(barIndex)) return false; } if(m_useADWyckoffFailedStructure) { if(!m_ADWyckoffFailedStructure.BufferResize(barIndex)) return false; } if(m_useADWyckoffSignificantBarInversion) { if(!m_ADWyckoffSignificantBarInversion.BufferResize(barIndex)) return false; } return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::RefreshData() { // CSeries/CIndicator::Refresh() is void - there is no per-call success/failure signal to // propagate here. The real data-validity check happens downstream, per value, in // BufferTempDataCompute() (EMPTY_VALUE / atr<=0 guards) - this function's job is only to ask // every buffer to refresh, unconditionally, before that per-value check runs. m_Open.Refresh(OBJ_ALL_PERIODS); m_Close.Refresh(OBJ_ALL_PERIODS); m_High.Refresh(OBJ_ALL_PERIODS); m_Low.Refresh(OBJ_ALL_PERIODS); if(m_useVolumes) { m_Volumes.Refresh(OBJ_ALL_PERIODS); } // Unconditional - see InitTime()'s call site in InitIndicators() for why m_Time must always be live. m_Time.Refresh(OBJ_ALL_PERIODS); if(m_useMA) { m_MA.Refresh(OBJ_ALL_PERIODS); } if(m_useRSI) { m_RSI.Refresh(OBJ_ALL_PERIODS); } if(m_useMACD) { m_MACDFeature.Refresh(OBJ_ALL_PERIODS); } if(m_useIchimoku) { m_Ichimoku.Refresh(OBJ_ALL_PERIODS); } // Unconditional - see the matching BufferResize() comment above. m_ATR.Refresh(OBJ_ALL_PERIODS); m_ADZigZag.Refresh(OBJ_ALL_PERIODS); if(m_useADCumulativeDelta) { m_ADCumulativeDelta.Refresh(OBJ_ALL_PERIODS); } if(m_useADShorteningOfThrust) { m_ADShorteningOfThrust.Refresh(OBJ_ALL_PERIODS); } if(m_useADWyckoffEventStream) { m_ADWyckoffEventStream.Refresh(OBJ_ALL_PERIODS); } if(m_useADWyckoffFailedStructure) { m_ADWyckoffFailedStructure.Refresh(OBJ_ALL_PERIODS); } if(m_useADWyckoffSignificantBarInversion) { m_ADWyckoffSignificantBarInversion.Refresh(OBJ_ALL_PERIODS); } return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Cache-or-compute wrapper around BufferTempDataCompute(): a given | //| now-relative bar index's feature vector is invariant until the | //| next candle close (see m_featureCache's declaration comment), so | //| a cache hit just replays the m_neuronsCount values already | //| computed for this idx straight into TempData instead of re- | //| deriving them from price/ATR/AD-indicator buffers again. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::BufferTempData(int idx) { int width = m_neuronsCount; bool cacheable = (idx >= 0 && idx < ArraySize(m_featureCacheHasValue) && width > 0); if(cacheable && m_featureCacheHasValue[idx]) { if(!m_featureCacheValid[idx]) return false; int base = idx * width; for(int f = 0; f < width; f++) if(!TempData.Add(m_featureCache[base + f])) return false; return true; } int startTotal = TempData.Total(); bool ok = BufferTempDataCompute(idx); //--- WIDTH CONTRACT. Every enabled feature block must emit exactly the number of values //--- m_neuronsCount was computed from, on every bar, unconditionally - a block that emits its //--- values on some bars and skips them on others (because an indicator, panel or series was //--- unavailable for THAT bar) does not merely shorten the window: it SHIFTS every feature after it //--- into the wrong slot, and the net then trains on silently misaligned inputs that still look like //--- a valid window to everything downstream. Caught here rather than left to surface as //--- BuildFeatureWindow's length check, which cannot say which bar or which block was responsible. if(ok) { int produced = TempData.Total() - startTotal; if(produced != width) { ok = false; m_featureFailTransient = false; // a width fault is structural, never "not ready yet" if(!m_featureWidthWarned) { m_featureWidthWarned = true; PrintFormat("%s: FEATURE WIDTH MISMATCH at bar %d - the enabled blocks produced %d values" " but m_neuronsCount says %d. Every feature after the short block would have" " landed in the wrong slot, so the bar is rejected rather than trained on." " A block that can be conditionally unavailable must emit neutral values, not" " nothing. Check the optional blocks first (cross-asset XA, spread SPR, swing" " context) - those are the ones with an availability test.", ID, idx, produced, width); } //--- Roll back the partial bar so the caller's window cannot contain half of it. while(TempData.Total() > startTotal) TempData.Delete(TempData.Total() - 1); } } //--- ONLY SUCCESSES ARE CACHED. A miss is never stored, in any form. //--- //--- The previous rule cached a miss whenever it was not flagged transient, and flagged exactly TWO //--- guards - the EMPTY_VALUE open and the cold ATR. That was the half-fix: every OTHER rejection in //--- BufferTempDataCompute (an indicator buffer not yet calculated, a panel not yet built, a series //--- not yet loaded, a failed Add) still cached as PERMANENT, so one early sweep across cold //--- indicators poisoned those bars for the rest of the process. Observed 2026-08-11: the MI //--- pre-scan runs ~3 s after OnInit, touches all 54k bars while the indicators are still warming, //--- and the run then reported "0 samples" and never trained again - the same failure the two-guard //--- version was written to prevent, arriving through the guards it did not cover. //--- //--- Enumerating which rejections are "really" permanent is the wrong shape of fix: it is a list that //--- has to be re-audited every time a feature block is added, and being wrong once costs the whole //--- run silently. Caching only successes needs no list and cannot be wrong. The cost is bounded and //--- small: in steady state the only bars that still fail are the handful at the deep end of history //--- inside the indicators' own warm-up, so an era recomputes ~ind_Periods bars rather than 54k. //--- //--- m_featureCacheValid is now always true where m_featureCacheHasValue is true. Both are kept //--- rather than collapsed into one array: the pair is written and read in several places, and a //--- silent meaning change is exactly how the last version of this drifted. if(cacheable && ok) { m_featureCacheHasValue[idx] = true; m_featureCacheValid[idx] = true; int base = idx * width; int count = TempData.Total() - startTotal; for(int f = 0; f < count && f < width; f++) m_featureCache[base + f] = TempData.At(startTotal + f); } return ok; } //+------------------------------------------------------------------+ //| THE ONE PLACE a feature WINDOW is assembled. Every consumer - | //| training pass 1/2/3, live inference, online learning, the OOS | //| continual simulation, the chart rescan and the CPU-inference | //| self-check - goes through here, because the thing this function | //| fixes is a contract that eight hand-rolled copies of the same | //| loop cannot hold on their own. | //| | //| ORDER IS CHRONOLOGICAL: OLDEST BAR FIRST, bar `r` (the bar being | //| predicted) LAST. That is the whole point of this function. | //| | //| MQL5 timeseries indices run BACKWARDS - index 0 is the newest bar | //| and increasing index walks into the past. So the obvious loop, | //| `for(b = 0..T-1) BufferTempData(r + b)`, appends the window in | //| REVERSE chronological order: the newest bar lands in block 0 and | //| the oldest in block T-1. That is what every call site used to do. | //| | //| For the dense (PAI) and convolutional stacks it is harmless - a | //| dense layer learns a weight per position either way, and a conv | //| just learns time-mirrored kernels. For the RECURRENT stacks it is | //| not, and it is not a subtlety: | //| - CNeuronLSTMOCL walks steps t = 0..T-1 reading `inputs + t*Iw` | //| (AI\Network.cl, LSTM_SeqStepForward), so step t consumes the | //| t-th block in buffer order. | //| - Its visible output is the LAST hidden state only - the kernel | //| writes `output[id]` solely when `t == steps - 1`. | //| - The cell state decays toward the start of the sequence: | //| c_t = f*c_{t-1} + i*g. DirectML\lstm_seq_flowcheck.cpp measured | //| block 0's influence on the output, relative to block T-1, at | //| 1.2e-2 for the shipped LSTM_FORGET_BIAS_INIT of 1.0 (see that | //| constant's comment for the full sweep). | //| Fed newest-first, that put the bar being PREDICTED at the far end | //| of the decay and handed the output to the OLDEST bar in the window | //| - roughly 80x backwards, and the exact inverse of what the window | //| exists for ("everything known as of this bar's close", see Train() | //| 's r comment). Reversing it here makes the final timestep the | //| current bar, which is the standard arrangement and the one the | //| forget-bias sweep was implicitly reasoning about. | //| | //| Nothing downstream reads a fixed block position, so this is safe | //| for every topology; it re-keys the weight fingerprint (see | //| ConfigFingerprint's WIN token) precisely BECAUSE the input vector | //| now means something different, and models trained under the old | //| order must never load into it. | //| | //| Returns true only when the COMPLETE, correctly-sized window is in | //| TempData - callers must not feedForward on a partial one (a stale | //| output layer would be scored against this bar's label). | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| ONE-SHOT FEATURE-VECTOR AUTOPSY, run the first time pass 1 finds | //| usable windows. Samples bars across the whole training range and | //| reports any feature slot that is DEGENERATE - constant, mostly | //| zero, or non-finite - naming the block it belongs to. | //| | //| WHY THIS EXISTS. Two separate 2026-08-17 failures were invisible | //| for hours because nothing ever looked at the assembled vector: | //| | //| - a cold ADMovingAverage returned EMPTY_VALUE for every index, | //| so every window was rejected and cached as a permanent miss. | //| The only symptom was a scan that never finished. | //| - the alt-data block 0-filled every bar older than its file's | //| first row (2010-01-01), which on USDJPY is ~half the history. | //| Training simply absorbed 15 impossible zeros per bar. | //| | //| Both are the same shape of bug: a block silently produces | //| nothing, and every downstream number stays plausible. A model | //| CANNOT tell "this feature is always 0" from "this feature is | //| genuinely 0 here", and neither can an accuracy figure - so the | //| check has to happen where the values are, once, and shout. | //| | //| Deliberately a REPORT, not a gate: a degenerate feature is | //| sometimes legitimate (a flag that is rare in this window), and | //| refusing to train would turn a diagnostic into an outage. It | //| names the problem and lets the operator decide. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ReportFeatureHealth(int bars) { if(m_featureHealthReported || m_neuronsCount <= 0) return; m_featureHealthReported = true; int per = m_neuronsCount; // features per BAR int lo = MathMax((int)m_historyBars + MathMax(m_barrierHorizonBars, 1) + 2, 2); int hi = MathMax(bars - 2, lo); if(hi <= lo) return; //--- Evenly spaced sample across the whole range, so a block that dies only in the deep history //--- (the alt-coverage case) is caught as surely as one that is dead everywhere (the cold-indicator //--- case). 400 bars is enough to call a feature constant and costs a fraction of one era. int want = 400; int step = MathMax((hi - lo) / want, 1); double vmin[], vmax[]; int zeroCnt[], seen = 0; ArrayResize(vmin, per); ArrayResize(vmax, per); ArrayResize(zeroCnt, per); for(int j = 0; j < per; j++) { vmin[j] = DBL_MAX; vmax[j] = -DBL_MAX; zeroCnt[j] = 0; } for(int i = lo; i <= hi; i += step) { //--- Read ONE bar's block, not a whole window: the per-bar row is what the blocks produce, and //--- BuildFeatureWindow would just replicate it historyBars times. TempData.Clear(); if(!BufferTempData(i)) continue; if(TempData.Total() < per) continue; //--- The bar's own row is the LAST `per` values (BufferTempData appends). int base = TempData.Total() - per; for(int j = 0; j < per; j++) { double v = TempData.At(base + j); if(!MathIsValidNumber(v)) continue; if(v < vmin[j]) vmin[j] = v; if(v > vmax[j]) vmax[j] = v; if(v == 0.0) zeroCnt[j]++; } seen++; } if(seen < 20) { Print(ID + StringFormat(": feature health - only %d of %d sampled bars produced a readable row;" " too few to judge. This is itself a warning: if it persists the feature" " path is rejecting nearly everything.", seen, want)); return; } //--- The alt block is appended LAST in every row (see BufferTempDataCompute), so its slots are the //--- final FeatureCount() of the `per`. Naming it matters: "slot 47 is constant" is a puzzle, //--- "the alt-data block is constant" is an answer. int altN = (m_useAltData ? m_altData.FeatureCount() : 0); int altFrom = (altN > 0 && altN <= per) ? per - altN : -1; string deadList = "", zeroList = ""; int dead = 0, mostlyZero = 0; for(int j = 0; j < per; j++) { if(vmin[j] > vmax[j]) continue; // never read bool isConst = (vmax[j] - vmin[j]) <= 1e-12; bool isZeroy = (zeroCnt[j] * 2 > seen); string tag = (altFrom >= 0 && j >= altFrom) ? StringFormat("alt[%d]", j - altFrom) : StringFormat("f%d", j); if(isConst) { dead++; if(dead <= 12) deadList += StringFormat("%s%s=%.4g", (deadList == "" ? "" : " "), tag, vmin[j]); } else if(isZeroy) { mostlyZero++; if(mostlyZero <= 12) zeroList += StringFormat("%s%s(%.0f%%)", (zeroList == "" ? "" : " "), tag, 100.0 * zeroCnt[j] / seen); } } Print(ID + StringFormat(": FEATURE HEALTH on %d sampled bars x %d features%s - %d CONSTANT%s%s |" " %d mostly-zero (>50%%)%s%s. A constant feature contributes nothing but" " still consumes a first-layer column and a BatchNorm slot; a block that is" " constant AND zero is usually a source that failed silently rather than a" " quiet market.", seen, per, (altFrom >= 0 ? StringFormat(" (alt block = slots %d..%d)", altFrom, per - 1) : " (no alt block)"), dead, (deadList == "" ? "" : ": "), deadList, mostlyZero, (zeroList == "" ? "" : ": "), zeroList)); } //+------------------------------------------------------------------+ bool CExpertSignalAIBase::BuildFeatureWindow(int r) { int width = (int)m_historyBars * m_neuronsCount; TempData.Clear(); TempData.Reserve(width); if(r < 0 || m_historyBars <= 0 || m_neuronsCount <= 0) return false; //--- Live-only freshness probe for the external block: two comparisons when quiet, a reload at //--- most hourly once the chart outruns the exported data. Never fires in the tester (the newest //--- bar is historical there). Safe against the feature cache: new alt rows only exist for a new //--- D1 bar, and a new bar resets the per-bar-frame cache anyway. if(m_useAltData) m_altData.EnsureFresh((datetime)m_Time.GetData(0)); //--- b counts bars BACK from r, so (m_historyBars - 1 - b) emits the deepest lookback first and //--- lands on r itself on the final iteration. Identical set of bars as before, opposite order. for(int b = 0; b < (int)m_historyBars; b++) if(!BufferTempData(r + ((int)m_historyBars - 1 - b))) { //--- Which lookback slot rejected, and how much of the window had been assembled. Without //--- this the pass-1 stall report can only say "0 of 54681 usable", which is true of a cold //--- ATR, a missing optional block and an out-of-range index alike. m_windowFailSlot = b; m_windowFailTotal = TempData.Total(); return false; } if(TempData.Total() < width) { //--- Nothing rejected the bar and the window is still short. Distinct fault from a guard //--- rejection and it used to be indistinguishable from one; the per-bar width contract in //--- BufferTempData should now catch this first, so reaching here means the shortfall is in the //--- window assembly itself rather than in one bar's blocks. m_windowFailSlot = -1; m_windowFailTotal = TempData.Total(); return false; } //--- THE ANCHOR BAR'S EXTERNAL READING ENTERS THE WINDOW ONCE, NOT ONCE PER BAR OF ITS DAY (2026-08-16). //--- //--- The alt block is appended LAST in every bar's row (see BufferTempDataCompute), and its lookup //--- is as-of by bar open time into a DAILY file - so every bar of the window that falls on the same //--- calendar day gets byte-identical values. Measured on the live SP500 D1 export (6073 rows, 13 //--- features, 5888 windows): each feature takes 1.7-2.7 distinct values across the 16 slots, 12 //--- principal components carry 95% of the block's variance and its effective rank is ~11.5. So 208 //--- inputs were carrying about 12 dimensions. Only 6 of the 13 features move daily at all; 5 are //--- weekly (COT, EIA) and 2 monthly (CPI, unemployment). //--- //--- The cost is NOT overfitting capacity - the copies are collinear, so they span ~12 directions, //--- not 208. It is the GRADIENT WEIGHTING. Batch norm standardizes each of the 208 coordinates //--- independently, which rescales the copies but does not decorrelate them, so the same factor //--- arrives on 16 unit-variance coordinates. Each of their weights takes a full-size step, and the //--- factor's aggregate coefficient therefore moves ~16x faster than a per-bar price feature's. The //--- network was structurally biased toward the external block by a factor of the window length - //--- an unintended prior, and pointing the wrong way, since these features cleared only a marginal //--- incremental screen while price features are the base signal. //--- //--- Zeroed here, at WINDOW ASSEMBLY, rather than in BufferTempData: that function's output is cached //--- PER BAR (m_featureCache) and a bar sits at slot 15 of one window and slot 0 of the next, so a //--- slot-dependent value there would either poison the cache or force a recompute on every slot. //--- The cache keeps the true values; only the copies in this window are cleared. The width contract //--- is untouched (same count, same positions) so conv/LSTM/HYBRID keep their bar-major rectangle and //--- need no change - and on an intraday chart the block arrives at the newest bar, which for the //--- LSTM is the final timestep, where a "current state of the world" reading belongs. Near-constant //--- coordinates are safe through batch norm: its divisor is //--- MathMax(MathSqrt(var + BN_EPSILON), BN_MIN_STD). //--- //--- ONLY THE ANCHOR'S OWN RUN IS COLLAPSED. Nothing older is touched, ever. //--- //--- The window's newest slot IS the bar being predicted (see the loop above - the final iteration //--- lands on r itself, and pass 3 grades that same index), so the reading in that slot is the one //--- contemporaneous with the decision, and it is the only one every alt screen actually validated. //--- This walks back from the anchor blanking bars that carry a BYTE-IDENTICAL copy of it, and stops //--- at the first bar that differs. Because an as-of lookup into a daily file is a step function in //--- time, those copies are exactly the contiguous run of bars sharing the anchor's calendar day. //--- //--- WHY IT STOPS THERE rather than deduplicating the whole window (2026-08-16, user's call before //--- deploy: "I would rather avoid lagging so the NN finds accurate patterns"). Collapsing every //--- distinct reading would leave each surviving at ONE slot - but WHICH slot depends on where the //--- day boundary falls inside that particular window, so a given lag would land on a different //--- coordinate from one window to the next. A dense layer holds a separate weight per (slot, //--- feature), so that turns a stable lagged input into a moving one. The older readings' natural //--- replicated runs are therefore left exactly as they were: whatever the net had learned to read //--- from those slots, it still reads from those same slots. The alt screens measured the CURRENT //--- reading's MI against forward range and never tested lags, so the lagged content is unproven - //--- and unproven is a reason to leave it undisturbed, not a licence to rearrange it. //--- //--- What this still buys: the anchor's reading reaches the first layer on ONE coordinate instead of //--- once per bar of its day, which removes the gradient upweight described above for the only //--- reading that was ever validated. It is also IDENTICAL to full deduplication exactly where the //--- replication was worst - on M15 and H1 the entire window sits inside one calendar day, so the //--- anchor's run IS the whole window - and a no-op on D1, where the bar immediately older than the //--- anchor is already a different day and the loop breaks on its first comparison. The only //--- timeframes where the two differ are the middle ones, and there this is the conservative choice. if(m_useAltData) { int an = m_altData.FeatureCount(); if(an > 0 && an <= m_neuronsCount && (int)m_historyBars > 1) { double anchor[]; ArrayResize(anchor, an); int newest = ((int)m_historyBars - 1) * m_neuronsCount + (m_neuronsCount - an); for(int k = 0; k < an; k++) anchor[k] = TempData.At(newest + k); for(int b = (int)m_historyBars - 2; b >= 0; b--) { int altBase = b * m_neuronsCount + (m_neuronsCount - an); bool same = true; for(int k = 0; k < an && same; k++) if(TempData.At(altBase + k) != anchor[k]) same = false; if(!same) break; // a different reading: this bar and everything older keep their values as-is for(int k = 0; k < an; k++) TempData.Update(altBase + k, 0.0); } } } return true; } //+------------------------------------------------------------------+ //| (Re)build the cross-asset panel over `bars` bars. | //| | //| Called from the same places that size the price buffers, because | //| the panel is aligned to exactly that bar grid and a stale panel | //| would silently mis-index. Cheap to call redundantly: Build() is | //| one CopyClose per reference pair, not per bar. | //| | //| A failure here is NOT fatal. The panel logs its own reason and | //| every Features() call then 0-fills, so the run continues without | //| the cross-asset block instead of refusing to train. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::BuildCrossAssetPanel(int bars) { if(!m_useCrossAsset) return true; if(bars <= 0) return false; //--- Deep enough AND anchored to the current newest bar. Depth alone would leave the panel's //--- index 0 pointing at a bar that is no longer the newest as soon as one candle closes, so //--- every cross-asset feature would be read one bar out of step with the price features beside //--- it - see m_crossAssetAnchor's declaration comment. datetime anchor = m_Time.GetData(0); if(m_crossAsset.IsReady() && m_crossAsset.Bars() >= bars && m_crossAssetAnchor == anchor && anchor > 0) return true; //--- A trained model builds from the pair set it was trained on (adopted from the .cfg), never //--- from whatever Market Watch holds today - see m_crossAssetPairsPinned. if(m_crossAssetPairsPinned != "" && !m_crossAsset.HasPinnedPairs()) m_crossAsset.SetPinnedPairs(m_crossAssetPairsPinned); if(!m_crossAsset.Build(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period, bars)) { m_crossAssetAnchor = 0; return false; } m_crossAssetAnchor = anchor; //--- FIRST successful build of a model with no pinned set yet: this pair set is now this model's //--- pair set for life. Stamp it and pin it to the .cfg one-shot, exactly like the derived barrier //--- pair (see Labels.mqh's m_geometryCfgSaved block) - the .cfg was written at model creation, //--- BEFORE the panel could possibly have built, so without this re-save the pin would live only //--- in memory and every restart would silently fall back to discovery. if(m_crossAssetPairsPinned == "" && m_crossAsset.UsedPairsCsv() != "") { m_crossAssetPairsPinned = m_crossAsset.UsedPairsCsv(); m_crossAsset.SetPinnedPairs(m_crossAssetPairsPinned); if(!m_crossAssetCfgSaved && m_activeFileName != "") { m_crossAssetCfgSaved = true; if(SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount, m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo, m_historyBars, m_outputNeuronsCount, m_neuronsCount, LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, m_isInitialized, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount, m_lstmHiddenSize, m_activeFileCommon)) Print(ID + ": cross-asset pair set PINNED to the .cfg - [" + m_crossAssetPairsPinned + "]. Restarts and redeploys now build the panel from exactly this set; Market Watch " "changes no longer alter what a trained model's features mean."); else Print(ID + ": WARNING - failed to pin the cross-asset pair set to the .cfg; a restart " "will re-discover Market Watch instead of adopting the trained set."); } } return true; } //+------------------------------------------------------------------+ //| Copy the historical spread series onto the current bar grid. | //| | //| CopySpread is a RANGE call, so this runs once wherever the price | //| buffers are sized - never per bar. Values are in POINTS (int); | //| the feature block converts with m_symbol.Point(). | //| | //| Non-fatal: a short or failed copy leaves m_spreadSeriesBars at | //| whatever was actually obtained and the feature block 0-fills past | //| it, matching the degraded-but-usable convention used by the swing | //| and cross-asset blocks. Refusing to train because one auxiliary | //| series came up short would be a far worse failure. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::EnsureSpreadSeries(int bars) { //--- The meta target's setup descriptor reads spread/ATR at the candidate's fire bar regardless of //--- whether spread is enabled as a per-bar WINDOW feature, so the series must exist for it. if(!m_useSpreadFeature && !IsMetaTarget()) return true; if(bars <= 0) return false; //--- Length alone is NOT a sufficient cache key - see m_spreadSeriesAnchor's declaration comment. datetime anchor = m_Time.GetData(0); if(m_spreadSeriesBars >= bars && m_spreadSeriesAnchor == anchor && anchor > 0) return true; ArraySetAsSeries(m_spreadSeries, true); // index 0 = newest, matching every other buffer here int got = CopySpread(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period, 0, bars, m_spreadSeries); if(got <= 0) { m_spreadSeriesBars = 0; m_spreadSeriesAnchor = 0; Print(__FUNCTION__ + ": CopySpread returned " + IntegerToString(got) + " for " + m_symbol.Name() + " - spread features 0-filled this run."); return false; } m_spreadSeriesBars = got; m_spreadSeriesAnchor = anchor; return true; } //+------------------------------------------------------------------+ bool CExpertSignalAIBase::BufferTempDataCompute(int idx) { //--- Where THIS bar's block starts. The function appends m_neuronsCount values below; remembering //--- the offset lets the whole vector be validated in one place at the end instead of at each of //--- the ~60 Add() call sites. int featureStart = TempData.Total(); //--- Cleared here, set by the two NOT-READY-YET guards below. See BufferTempData() for what it //--- controls: a rejection caused by data that has not arrived yet must not be cached, because the //--- cache never re-tries a miss. m_featureFailTransient = false; //--- Cleared alongside it, and written by every guard below that can return false - see the //--- declaration for why a value COUNT was never enough to identify the block. m_featureFailBlock = ""; m_featureFailIdx = idx; double open = m_Open.GetData(idx); double close = m_Close.GetData(idx); double high = m_High.GetData(idx); double low = m_Low.GetData(idx); MqlDateTime sTime; TimeToStruct(m_Time.GetData(idx), sTime); if(open == EMPTY_VALUE) { m_featureFailTransient = true; m_featureFailBlock = "price/open (m_Open.GetData == EMPTY_VALUE)"; return false; } // ATR-normalize every raw-price-unit feature below instead of feeding e.g. 0.0005 on EURUSD vs. // 50.0 on a JPY pair or an index straight into the network - with Adam and hardcoded, scale- // sensitive activations (TANH saturates, PRELU's 0.01 leak only means anything relative to the // input's own scale), an unnormalized feature either vanishes into rounding noise or dominates // the weighted sum depending on which symbol/timeframe happens to be loaded. Dividing by the // bar's own ATR expresses every price-based feature as "fraction of typical volatility", which // is comparable across symbols/timeframes and centered near zero. No ATR reading yet (e.g. the // first few bars of history) means every price feature this bar would be meaningless - reject // the bar via the same "return false" convention as the EMPTY_VALUE check above. double atr = m_ATR.Main(idx); if(atr <= 0.0 || atr == EMPTY_VALUE) { //--- TRANSIENT BY NATURE, and the reason resumed models could never train. MT5 calculates an //--- indicator's buffers asynchronously after the handle is created, so a call made before ATR //--- has filled returns 0 for EVERY index, not just the warm-up tail. A FRESH model never saw //--- this: it sits through m_warmupPassesRemaining separately-scheduled Train() calls before //--- anything touches a feature, which is exactly what those passes are for. A RESUMED model //--- skips them - TuneIndicatorsAndTrain drives StartLabelCachePrebuild and the MI report from //--- the very first chart event, milliseconds after OnInit - so it read a cold ATR, every bar //--- was rejected, and BufferTempData cached all of it as permanent misses. From then on //--- BuildFeatureWindow failed on every bar of every era, add_loop never went true, and pass 1 //--- swept 0->100% forever with nothing in the journal (2026-08-10; deleting the .nnw "fixed" //--- it only by turning the model back into a fresh one). m_featureFailTransient = true; m_featureFailBlock = StringFormat("ATR (m_ATR.Main=%.10g, needs > 0)", atr); return false; } if(!TempData.Add((close - open) / atr) || !TempData.Add((high - open) / atr) || !TempData.Add((low - open) / atr) || // Explicit bullish/bearish flag - (close-open)/atr already encodes direction *and* magnitude // together, which asks the network to disentangle "which way" from "how much" out of a single // continuous value. Giving direction its own clean +1/-1/0 signal removes that ambiguity. !TempData.Add(close > open ? 1.0 : (close < open ? -1.0 : 0.0))) { return false; } if(m_useSwingContext) { // Most recent CONFIRMED swing pivot as of bar idx - "confirmed" meaning at least // m_swingConfirmationBars MORE bars have closed after it (see m_swingConfirmationBars' and // m_useSwingContext's declaration comments). This is now the ONLY consumer of that embargo: the // label side stopped needing it when the target became the triple barrier, whose own lookahead is // m_barrierHorizonBars. Skipping this embargo here - e.g. reading // m_ADZigZag's raw current buffer value instead - would leak information a live bar at idx // could never actually have had yet, since ZigZag's most recent 1-3 legs are still provisional // and can be revised as new bars arrive. int pivotIdx = -1; double pivotPrice = 0.0; bool pivotIsLow = false; if(!FindConfirmedZigZagPivot(idx + MathMax(m_swingConfirmationBars, 1), pivotIdx, pivotPrice, pivotIsLow)) { // No confirmed pivot within the scan cap (e.g. right at the start of available history) - // this is legitimately "no swing context yet", not bad/missing data, so a neutral 0-fill // keeps the bar usable rather than rejecting it outright like the ATR/EMPTY_VALUE guards do. if(!TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0)) return false; } else { // Direction of the CURRENT leg: the last confirmed pivot being a bottom means price has been // rising away from it (an up-leg) ever since, and vice versa - same +1/-1 convention as the // bullish/bearish flag above, just at swing scale instead of single-bar scale. double direction = pivotIsLow ? 1.0 : -1.0; // How far price has travelled since that pivot, ATR-normalized and signed (+ve above the // pivot price, -ve below) - clamped generously since an extended trending leg has no natural // ceiling the way a single bar's range does. double distSincePivot = MathMax(-10.0, MathMin(10.0, (close - pivotPrice) / atr)); // Magnitude of the PRIOR completed leg (the pivot immediately before pivotIdx) - a scale // reference for "is the current move big or small relative to the last full swing". No // additional embargo needed here (see FindConfirmedZigZagPivot()'s declaration comment) - // anything at or before an already-confirmed pivot is necessarily even older. int priorPivotIdx = -1; double priorPivotPrice = 0.0; bool priorPivotIsLow = false; bool havePrior = FindConfirmedZigZagPivot(pivotIdx + 1, priorPivotIdx, priorPivotPrice, priorPivotIsLow); double priorLegMagnitude = havePrior ? MathMax(0.0, MathMin(10.0, MathAbs(pivotPrice - priorPivotPrice) / atr)) : 0.0; // Retracement/extension ratio (current distance relative to the prior leg's own size) - // Fibonacci-style relative position, often more informative than either raw magnitude alone // since it's comparable across both quiet and volatile regimes. 0 when there's no prior leg // to compare against yet. double retracementRatio = (havePrior && priorLegMagnitude > 0.0001) ? MathMax(-5.0, MathMin(5.0, distSincePivot / priorLegMagnitude)) : 0.0; // Swing age (bars since the pivot) - a maturity/exhaustion proxy, same +/- style clamp // convention as the volume-ratio feature below. double barsSincePivot = MathMax(0.0, MathMin(5.0, (double)(pivotIdx - idx) / 100.0)); if(!TempData.Add(direction) || !TempData.Add(distSincePivot) || !TempData.Add(priorLegMagnitude) || !TempData.Add(retracementRatio) || !TempData.Add(barsSincePivot)) return false; } // Recent price-action context (4 features), computed from CLOSED bars at idx or older only - // no ZigZag confirmation, so no repainting and NO embargo, and never stale, unlike the five // pivot-anchored features above whose confirmed anchor is always >= m_swingConfirmationBars // (~100) bars old. Those describe the OLD structure well but say nothing about the recent leg // the bar actually sits in - which is exactly what's needed to tell a genuine reversal at a // range extreme from a mid-trend bar that merely looks like a bottom/top (the "clustered // counter-trend signals" failure mode). These locate the bar within its recent range and // trend so the network can learn that a directional call belongs at an extreme of an extended // move, not anywhere the local candle shape resembles a pivot. All windows walk toward OLDER // bars (increasing index), so nothing here can see the future. double hi20 = high, lo20 = low, hi50 = high, lo50 = low; double sum20 = close, oldestClose20 = close; int cnt20 = 1; for(int w = 1; w < 50; w++) { int j = idx + w; double jc = m_Close.GetData(j); double jh = m_High.GetData(j); double jl = m_Low.GetData(j); // ran off the oldest edge of loaded history (out-of-range reads back as 0/EMPTY_VALUE) - // use whatever window we gathered so far rather than rejecting the bar; a shorter early- // history window is degraded-but-usable, same spirit as the pivot 0-fill above. if(jh == EMPTY_VALUE || jh <= 0.0 || jl <= 0.0) break; if(jh > hi50) hi50 = jh; if(jl < lo50) lo50 = jl; if(w < 20) { if(jh > hi20) hi20 = jh; if(jl < lo20) lo20 = jl; sum20 += jc; oldestClose20 = jc; cnt20++; } } // Donchian position: where close sits inside the recent high/low range, rescaled to [-1,+1] // (-1 = at the range low / bottom candidate, +1 = at the range high / top candidate, 0 = mid- // range / mid-trend). Two scales - a short 20-bar and a medium 50-bar view - so the network // sees both local and swing-scale extremity. 0 (mid) when the range is degenerate. double range20 = hi20 - lo20; double range50 = hi50 - lo50; double donchPos20 = (range20 > 0.0) ? ((close - lo20) / range20 - 0.5) * 2.0 : 0.0; double donchPos50 = (range50 > 0.0) ? ((close - lo50) / range50 - 0.5) * 2.0 : 0.0; // Net directional displacement over the recent window, ATR-normalized and signed - the // prevailing-trend strength/direction the counter-trend clusters were ignoring. double recentReturn = MathMax(-10.0, MathMin(10.0, (close - oldestClose20) / atr)); // Distance from the recent mean (SMA), ATR-normalized - a stretch/exhaustion proxy distinct // from the net return (a move can be far from its mean with little net displacement, or vice // versa); genuine reversals tend to be over-extended from equilibrium. double smaExtension = MathMax(-10.0, MathMin(10.0, (close - sum20 / cnt20) / atr)); if(!TempData.Add(donchPos20) || !TempData.Add(donchPos50) || !TempData.Add(recentReturn) || !TempData.Add(smaExtension)) return false; } if(m_useVolumes) { // FOUR values, not one. This block used to feed only the bar-over-bar change ratio below. // research/test_volume.py measured all four against the barrier label with a block-permutation // null (blocks = the barrier horizon, because adjacent labels share almost their whole outcome // window and a free shuffle produces a null far too tight): the LEVEL and the two // volume-vs-range interactions each carry information the first difference does not, and the // level beats the shipped feature outright on 4 of 6 instrument/geometry cells. // // Read the magnitudes before expecting much: the excess mutual information is ~2e-4 nats // against a label entropy near 1.05, i.e. well under a tenth of one percent of the label's // uncertainty. This is real and repeatable across instruments, and it is nowhere near an edge. // It is worth having because it costs one 50-bar loop, not because it changes the answer. double vNow = m_Volumes.Main(idx); double prevVolume = m_Volumes.Main(idx + 1); double volumeDelta = vNow - prevVolume; // Relative change - trading activity magnitude varies wildly across symbols/timeframes, so the // previous bar's own volume is the scale reference, same logic as ATR-normalizing price above. // Guard against a zero previous-bar volume (e.g. a holiday-thin session) instead of dividing by // it. Clamped to +/-5: unlike the ATR-normalized price features this ratio has no natural // ceiling (a 1-tick bar followed by a normal one produces a huge outlier). double volumeChangeRatio = prevVolume > 0.0 ? volumeDelta / prevVolume : 0.0; // Baseline over the trailing 50 bars, walking toward OLDER bars only (increasing index), so // nothing here can see the future. Degraded-but-usable at the oldest edge, same convention as // the swing-context window above: a short early-history baseline beats rejecting the bar. double volSum = vNow; int volCnt = 1; for(int w = 1; w < 50; w++) { double jv = m_Volumes.Main(idx + w); if(jv <= 0.0) break; volSum += jv; volCnt++; } double volBase = volSum / volCnt; // LEVEL: is this an active bar or a dead one? The change ratio cannot express this at all - // two consecutive dead bars and two consecutive frantic ones both read as ~0 change. double volLevel = (volBase > 0.0) ? vNow / volBase : 1.0; double rangeAtr = (high - low) / atr; // ABSORPTION: range delivered per unit of activity. A low value means heavy participation that // went nowhere - supply meeting demand - which is a categorically different bar from heavy // participation that travelled. The single change ratio conflates the two. double absorption = (volLevel > 0.05) ? rangeAtr / volLevel : 0.0; // ...and its converse, effort AND result together, which is the continuation reading. double volXrange = volLevel * rangeAtr; if(!TempData.Add(MathMax(-5.0, MathMin(5.0, volumeChangeRatio))) || !TempData.Add(MathMax(0.0, MathMin(5.0, volLevel))) || !TempData.Add(MathMax(0.0, MathMin(5.0, absorption))) || !TempData.Add(MathMax(0.0, MathMin(5.0, volXrange)))) return false; } if(m_useTime) { // Normalize time (cyclical encoding) if(!TempData.Add(sin(2 * M_PI * sTime.hour / 24.0))) return false; if(!TempData.Add(cos(2 * M_PI * sTime.hour / 24.0))) return false; if(!TempData.Add(sin(2 * M_PI * sTime.day_of_week / 7.0))) return false; if(!TempData.Add(cos(2 * M_PI * sTime.day_of_week / 7.0))) return false; if(!TempData.Add(sin(2 * M_PI * sTime.mon / 12.0))) return false; if(!TempData.Add(cos(2 * M_PI * sTime.mon / 12.0))) return false; } if(m_useATR) { // ATR/close (volatility as a fraction of price), not raw ATR - the raw absolute value is // itself unnormalized (e.g. ~0.0012 on EURUSD vs. ~1.5 on gold, and drifts over time even on // one symbol as its price level changes), which is exactly the kind of scale-dependent // feature this whole normalization pass is fixing everywhere else. if(!TempData.Add(close != 0.0 ? atr / close : 0.0)) return false; } if(m_useMA) { // Same ATR-normalized distance-from-level convention as the base OHLC-from-open features above, // just measured against the MA instead of the bar's own open - lets the network read where // price sits relative to the same MA Signals\SignalMA.mqh votes on. Plus the MA's own // bar-over-bar change (also ATR-normalized, since the MA lives in price units and ATR is // already this codebase's scale reference for that - see m_useMA's declaration comment for why // this isn't volume's previous-bar-ratio scheme instead). double maNow = m_MA.GetData(0, idx); double maPrev = m_MA.GetData(0, idx + 1); if(maNow == EMPTY_VALUE || maPrev == EMPTY_VALUE) { //--- TRANSIENT, for exactly the reason spelled out at the ATR guard above, and this is the //--- guard that proved it: 2026-08-17, six fresh instances on USDJPY and XAUUSD swept //--- 33,965-50,162 bars and produced ZERO usable windows, over and over, for 40 minutes. //--- The stall report named the spot precisely - "lookback slot 0 REJECTED (window had 24 of //--- 832 values)" - and 24 is exactly the core block (4 price + 5 swing + 4 range + 4 volume //--- + 6 time + 1 ATR), so feature 25, the first value of THIS block, was the wall. //--- ADMovingAverage is a CUSTOM indicator: MT5 fills its buffer asynchronously, so before it //--- has calculated it returns EMPTY_VALUE for EVERY index, not just the warm-up tail. Without //--- this flag every bar of the sweep was cached as a PERMANENT miss, which is the identical //--- failure the ATR guard above was fixed for on 2026-08-10 - the fix was simply never //--- propagated to the indicator blocks that follow it. //--- Worse, it self-sustained: the discarded era immediately re-swept all 50k bars, and six //--- instances doing that in a loop on a six-core box starved the very indicator they were //--- waiting on. Marking it transient is what lets the cache retry instead of poisoning. m_featureFailTransient = true; //--- WHICH of the two reads failed, and whether the indicator is empty EVERYWHERE or only here. //--- Those are different faults with different fixes and the old report could not separate them: //--- newest bar also EMPTY -> the whole buffer is unreadable (cold, or a dead/invalid handle). //--- Every anchor in the sweep fails, ok=0, and no amount of re-sweeping helps. //--- newest bar READS -> the buffer is fine and only this depth is missing, i.e. a genuine //--- history edge. Recent anchors succeed and ok > 0, so a total failure RULES THIS OUT. //--- ok=0 across 50,163 anchors on 2026-08-17 therefore meant the first case, which is exactly //--- what "the handle is short on deep history" would NOT produce - and that is the reading the //--- report has to hand over instead of leaving to be inferred. string maNewest = "reads (buffer live; this is a history-edge miss)"; if(m_MA.GetData(0, 0) == EMPTY_VALUE) maNewest = "ALSO EMPTY (whole buffer unreadable - cold or dead handle, NOT a depth shortfall)"; m_featureFailBlock = StringFormat("MA (ADMovingAverage) - GetData(%d)=%s GetData(%d)=%s," " newest bar %s, BarsCalculated=%d", idx, maNow == EMPTY_VALUE ? "EMPTY" : "ok", idx + 1, maPrev == EMPTY_VALUE ? "EMPTY" : "ok", maNewest, m_MA.BarsCalculated()); return false; } if(!TempData.Add((open - maNow) / atr) || !TempData.Add((high - maNow) / atr) || !TempData.Add((low - maNow) / atr) || !TempData.Add((close - maNow) / atr) || !TempData.Add((maNow - maPrev) / atr)) return false; } if(m_useRSI) { // Already a 0-100 oscillator - /100 is the only transform needed to match the rest of the // feature vector's scale (see m_useRSI's declaration comment). double rsiNow = m_RSI.Main(idx); if(rsiNow == EMPTY_VALUE) { m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above m_featureFailBlock = StringFormat("RSI - Main(%d)=EMPTY, newest bar %s, BarsCalculated=%d", idx, m_RSI.Main(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads", m_RSI.BarsCalculated()); return false; } if(!TempData.Add(rsiNow / 100.0)) return false; } if(m_useMACD) { // Main and signal lines are price-domain differences of two EMAs, so the same ATR normalization // every other price-unit feature here uses applies unchanged. The third value is the histogram // (main - signal): algebraically derivable from the first two, but handed over explicitly for the // same reason the bullish/bearish flag is handed to the network alongside (close-open)/atr - a // value the network would otherwise have to learn to subtract is better given directly, and the // histogram (momentum ACCELERATION) is the one term nothing else in this vector carries. double macdMain = m_MACDFeature.Main(idx); double macdSignal = m_MACDFeature.Signal(idx); if(macdMain == EMPTY_VALUE || macdSignal == EMPTY_VALUE) { m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above m_featureFailBlock = StringFormat("MACD - Main(%d)=%s Signal(%d)=%s, newest bar %s," " BarsCalculated=%d", idx, macdMain == EMPTY_VALUE ? "EMPTY" : "ok", idx, macdSignal == EMPTY_VALUE ? "EMPTY" : "ok", m_MACDFeature.Main(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads", m_MACDFeature.BarsCalculated()); return false; } if(!TempData.Add(macdMain / atr) || !TempData.Add(macdSignal / atr) || !TempData.Add((macdMain - macdSignal) / atr)) return false; } if(m_useIchimoku) { // LOOKAHEAD, the one thing that matters in this block. MT5's iIchimoku does NOT pre-shift its // buffers - it stores raw per-bar values and shifts only the DRAWING (Ichimoku.mq5 sets // PLOT_SHIFT=+Kijun on the Senkou A/B cloud plot and -Kijun on the Chikou plot). In series // indexing that means: // - SenkouSpan*(i) is computed FROM bar i and drawn Kijun bars into the FUTURE, so the cloud // actually sitting under bar idx is SenkouSpan*(idx + kijun) - built from bar idx+kijun and // older, hence strictly past data. Reading SenkouSpan*(idx) as "the cloud here" is the classic // Ichimoku backtest bug and would leak Kijun bars of future information into every example. // - SenkouSpan*(idx) with NO offset IS legitimate as the PROJECTED cloud - the part of the chart // already drawn ahead of the current bar. A live bar at idx genuinely knows it (it is computed // from bar idx), which is why it appears below as its own feature rather than being avoided. // - ChinkouSpan(i) is just Close(i) drawn at i+Kijun, so the Chikou plotted AT bar idx would be // Close(idx - kijun) - a FUTURE bar. It is never read. The lookahead-free statement of the same // reading is "how far is this close from the close Kijun bars ago", the last feature below. // Signals\SignalIchimoku.mqh's class comment documents the identical convention for the vote side. int kijunShift = m_indicatorTuner.ichiKijun; double tenkan = m_Ichimoku.TenkanSen(idx); double kijun = m_Ichimoku.KijunSen(idx); double spanA = m_Ichimoku.SenkouSpanA(idx + kijunShift); // cloud AS PLOTTED AT bar idx double spanB = m_Ichimoku.SenkouSpanB(idx + kijunShift); double futureSpanA = m_Ichimoku.SenkouSpanA(idx); // cloud projected AHEAD of bar idx double futureSpanB = m_Ichimoku.SenkouSpanB(idx); double closeLagRef = m_Close.GetData(idx + kijunShift); // Chikou reference, never idx - kijunShift if(tenkan == EMPTY_VALUE || kijun == EMPTY_VALUE || spanA == EMPTY_VALUE || spanB == EMPTY_VALUE || futureSpanA == EMPTY_VALUE || futureSpanB == EMPTY_VALUE || closeLagRef == EMPTY_VALUE || closeLagRef <= 0.0) { m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above //--- closeLagRef is called out separately because it is the one term here that reads the CLOSE //--- series at idx + kijunShift, so it fails on the oldest kijunShift bars by construction (see //--- ResizeBuffers' clamp note) rather than because Ichimoku is unready. m_featureFailBlock = StringFormat("Ichimoku - tenkan=%s kijun=%s spanA=%s spanB=%s fA=%s fB=%s" " closeLag(idx+%d)=%s, newest bar %s, BarsCalculated=%d", tenkan == EMPTY_VALUE ? "EMPTY" : "ok", kijun == EMPTY_VALUE ? "EMPTY" : "ok", spanA == EMPTY_VALUE ? "EMPTY" : "ok", spanB == EMPTY_VALUE ? "EMPTY" : "ok", futureSpanA == EMPTY_VALUE ? "EMPTY" : "ok", futureSpanB == EMPTY_VALUE ? "EMPTY" : "ok", kijunShift, (closeLagRef == EMPTY_VALUE || closeLagRef <= 0.0) ? "EMPTY" : "ok", m_Ichimoku.TenkanSen(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads", m_Ichimoku.BarsCalculated()); return false; } if(!TempData.Add((close - tenkan) / atr) || // distance to the fast line !TempData.Add((close - kijun) / atr) || // distance to the equilibrium line !TempData.Add((tenkan - kijun) / atr) || // TK spread: sign = cross state, size = conviction !TempData.Add((close - spanA) / atr) || // distance to each cloud edge, so the network can !TempData.Add((close - spanB) / atr) || // place price above / inside / below the cloud !TempData.Add((spanA - spanB) / atr) || // signed cloud thickness here: sign = regime, size = strength !TempData.Add((futureSpanA - futureSpanB) / atr) || // same for the projected cloud - the "twist" ahead !TempData.Add((close - closeLagRef) / atr)) // Chikou displacement, in its lookahead-free form return false; } if(m_useNews) { // Event proximity + impact only - see this member's declaration comment and // System\NewsRelevance.mqh's ImpactWeightedProximity() for why the forward-looking half // (searchForward=true) isn't lookahead bias despite being computed for a historical bar. // What is DELIBERATELY not here is actual-vs-forecast surprise. Release TIMES are published // in advance and never revised, so reading them for a historical bar is legitimate; released // VALUES are neither. MqlCalendarValue.actual_value returns the FINAL figure, and the calendar // keeps no as-of-release snapshot (revised_prev_value exists precisely because revisions // happen), so a surprise feature computed for a 2019 bar would be built from a number nobody // had in 2019. That is the same class of leak that made the RSI/MACD divergence models read // +4 sigma in research/test_classic.py until two bars of lookahead were closed - except this // one would survive into production and be paid for in real money. datetime barTime = m_Time.GetData(idx); double newsRecency = ImpactWeightedProximity(m_symbol.Name(), barTime, m_newsFeatureWindowMinutes, false); double newsProximity = ImpactWeightedProximity(m_symbol.Name(), barTime, m_newsFeatureWindowMinutes, true); if(!TempData.Add(newsRecency) || !TempData.Add(newsProximity)) return false; } if(m_useSpreadFeature) { // TWO values. What this block actually encodes is worth stating precisely, because the raw // measurement overstates it. // // spr/atr measured as the single strongest feature in this codebase (research/test_spread.py): // significant on 5 of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier // LABEL is computed with the spread charged inside it, so a high-spread bar has its barriers // shifted adversely and is mechanically likelier to resolve as a loss - the feature would // partly be predicting its own cost model, which is not tradeable. Re-labelling at zero cost // and re-measuring the identical feature showed 20-40% of the signal WAS that tautology and // the majority was not (XAUUSD kept 97%). // // What survives is a VOLATILITY-REGIME reading: the spread is near-fixed while ATR is not, so // this ratio runs high exactly when realised volatility is below its own ATR estimate - which // genuinely predicts whether ATR-scaled barriers get reached at all. Note it is UNSIGNED, like // volume: it informs Neutral-vs-directional and can never pick a side. double sprRatio = 0.0, sprChange = 0.0; if(idx + 1 < m_spreadSeriesBars) { double sNow = (double)m_spreadSeries[idx] * m_symbol.Point(); double sPrev = (double)m_spreadSeries[idx + 1] * m_symbol.Point(); sprRatio = sNow / atr; if(sPrev > 0.0) sprChange = (sNow - sPrev) / sPrev; } if(!TempData.Add(MathMax(0.0, MathMin(5.0, sprRatio))) || !TempData.Add(MathMax(-5.0, MathMin(5.0, sprChange)))) return false; } if(m_useCrossAsset) { // What every OTHER instrument was doing at this bar's timestamp - the one feature block here // that is not a function of this symbol's own series. See System\CrossAsset.mqh. // A panel that failed to build (no Market Watch pairs, unsynchronised history) yields a // neutral 0-fill rather than rejecting the bar: the block is additive context, and losing // every bar of training because a reference symbol was missing would be a far worse failure // than training without the context. Features() reports that by returning false, which is // logged once at build time rather than per bar. double xa[]; m_crossAsset.Features(idx, xa); for(int k = 0; k < CROSSASSET_FEATURES; k++) if(!TempData.Add(xa[k])) return false; } if(m_useADCumulativeDelta) { //--- COLD IS TRANSIENT, NOT ZERO (2026-08-11). These raw GetData reads have no EMPTY_VALUE //--- guard of their own; a not-yet-calculated indicator returns EMPTY_VALUE for EVERY index, //--- the sanitize loop at the bottom rewrites that to 0.0, the bar then SUCCEEDS - and //--- BufferTempData caches it as a success for the whole bar frame. That is the one path the //--- f6150ee only-cache-successes rule cannot see, because it never fails: on a resumed model //--- the era-0 prebuild/MI report start milliseconds after OnInit and could train on all-zero //--- Wyckoff/AD blocks for up to a full bar (the ba13eef failure class, arriving through //--- values that never fail). ADIndicatorCold probes the NEWEST bar: EMPTY_VALUE there means //--- the async calculation hasn't filled yet -> transient reject (never cached, retried like //--- the cold-ATR guard above). A warm indicator whose DEEP bars read EMPTY_VALUE (beyond its //--- buffer depth) is different - that stays the sanitize loop's neutral-fill, since rejecting //--- those bars would starve training of legitimately degraded history. if(ADIndicatorCold(m_ADCumulativeDelta, "ADCumulativeDelta")) return false; // buffers: 0=Pressure, 1=CumulativeDelta, 2=BullishPressure, 3=BearishPressure, 4=Absorption, 5=Initiative. // CumulativeDelta (buffer 1) is now cumulativeDelta/sumVolume clamped +/-2 (same scale as every // other buffer here, see ADCumulativeDelta.mq5) - a pure order-flow-imbalance ratio, distinct from // Pressure (buffer 0), which is this same term further adjusted by Initiative/Absorption. if(!TempData.Add(m_ADCumulativeDelta.GetData(0, idx)) || // Pressure !TempData.Add(m_ADCumulativeDelta.GetData(1, idx)) || // CumulativeDelta !TempData.Add(m_ADCumulativeDelta.GetData(2, idx)) || // BullishPressure !TempData.Add(m_ADCumulativeDelta.GetData(3, idx)) || // BearishPressure !TempData.Add(m_ADCumulativeDelta.GetData(4, idx)) || // Absorption !TempData.Add(m_ADCumulativeDelta.GetData(5, idx))) // Initiative return false; } if(m_useADShorteningOfThrust) { if(ADIndicatorCold(m_ADShorteningOfThrust, "ADShorteningOfThrust")) // see the CumulativeDelta block's comment return false; // buffers: 0=SOT, 1=SOTEffortRegime, 2=SOTConfirmation, 3=SOTPushRegime if(!TempData.Add(m_ADShorteningOfThrust.GetData(0, idx)) || // SOT !TempData.Add(m_ADShorteningOfThrust.GetData(1, idx)) || // SOTEffortRegime !TempData.Add(m_ADShorteningOfThrust.GetData(2, idx)) || // SOTConfirmation !TempData.Add(m_ADShorteningOfThrust.GetData(3, idx))) // SOTPushRegime return false; } if(m_useADWyckoffEventStream) { // buffers: 0=EventCode, 1=EventPhase, 2=ZoneTop, 3=ZoneBottom, 4=EventPrice, 5=StructuralPhase, // 6=CHoCHTrendToRange, 7=CHoCHRangeToTrend, 8=SlopeAccumulationBullish, 9=SlopeAccumulationBearish, // 10=SlopeDistributionBullish, 11=SlopeDistributionBearish, 12=Reaccumulation, 13=Redistribution. // Buffer 4 (EventPrice) is deliberately skipped below - per the indicator's own source // (ADWyckoffEventStream.mq5: "BufColor[wi]=(ev!=0)?C[i]:0;"), it's just this bar's close price // echoed back when an event fires (0 otherwise), kept only so a charting/backtesting tool like // StrategyQuant can anchor an arrow to a price. It carries no information the network doesn't // already have (EventCode already flags whether an event fired; the close is already in the // base OHLC features), and normalizing it as a "distance from close" like ZoneTop/ZoneBottom // below would be actively wrong: it's close-close=0 on event bars but 0-close=-close (a raw, // ATR-blown-up price) on every other bar - a huge, meaningless outlier feature. // Buffer 1 (EventPhase) USED to be skipped for the same kind of reason - it was written as // "BufPhase[wi]=(double)ev;", a byte-for-byte copy of EventCode. The 2026-08-02 rewrite made it a // real reading: "BufPhase[wi]=(double)(phaseNow*((dirNow>=0)?1:-1))", i.e. the live range's own // Wyckoff phase 1..5 signed by whether that range is accumulation (+) or distribution (-). That is // NOT what StructuralPhase (buffer 5) carries: StructuralPhase is derived from the EVENT on this // bar (MapStructuralPhase(ev)) and so is 0 on every bar where nothing fires, while EventPhase // persists for the whole life of the range. The pair gives the network both "an event just put us // in phase C" and "we are still in phase C" - so it is included below. // SIGN AND MAGNITUDE SPLIT (2026-08-09 audit, N1). All three of these buffers are signed // categoricals of the form (stage * direction), packed into one scalar: // EventCode +-1..7 sign = accumulation/distribution, |v| = the Wyckoff schematic stage // (1 PS, 2 SC, 3 AR, 4 ST, 5 Spring/UTAD, 6 LPS/LPSY, 7 SOS/SOW) // EventPhase +-1..5 the LIVE range's phase, same sign convention, persists between events // StructuralPhase +-1..5 this bar's event mapped to a phase, 0 when nothing fired // Fed raw, each one asks the network to disentangle "which way" from "how far through the // schematic" out of a single continuous value - and to do it across a sign change, where the // ordinal jumps from -1 to +1 with nothing in between. That is the exact ambiguity the base OHLC // block calls out and fixes by handing direction its own +1/-1/0 flag beside (close-open)/atr; // these are the same shape of value and get the same treatment. // Information-preserving: (dir, mag) reconstructs the original exactly, so this is a re-encoding // and not a feature change. Magnitudes are scaled onto [0,1] by their own maxima so they sit in // the same range as the rest of the vector instead of reaching 7. // The magnitudes ARE meaningfully ordinal - the schematic is a sequence, not a set of unrelated // labels - which is why they stay scalars rather than being one-hot expanded across 7 inputs. if(ADIndicatorCold(m_ADWyckoffEventStream, "ADWyckoffEventStream")) // see the CumulativeDelta block's comment return false; double wesEvent = m_ADWyckoffEventStream.GetData(0, idx); double wesLivePhase = m_ADWyckoffEventStream.GetData(1, idx); double wesStructPhase = m_ADWyckoffEventStream.GetData(5, idx); if(!TempData.Add(wesEvent > 0 ? 1.0 : (wesEvent < 0 ? -1.0 : 0.0)) || // event direction !TempData.Add(MathMin(1.0, MathAbs(wesEvent) / 7.0)) || // event stage !TempData.Add(wesLivePhase > 0 ? 1.0 : (wesLivePhase < 0 ? -1.0 : 0.0)) || // live-range direction !TempData.Add(MathMin(1.0, MathAbs(wesLivePhase) / 5.0)) || // live-range phase !TempData.Add((m_ADWyckoffEventStream.GetData(2, idx) - close) / atr) || // ZoneTop !TempData.Add((m_ADWyckoffEventStream.GetData(3, idx) - close) / atr) || // ZoneBottom !TempData.Add(wesStructPhase > 0 ? 1.0 : (wesStructPhase < 0 ? -1.0 : 0.0)) || // struct direction !TempData.Add(MathMin(1.0, MathAbs(wesStructPhase) / 5.0)) || // struct phase !TempData.Add(m_ADWyckoffEventStream.GetData(6, idx)) || // CHoCHTrendToRange !TempData.Add(m_ADWyckoffEventStream.GetData(7, idx)) || // CHoCHRangeToTrend !TempData.Add(m_ADWyckoffEventStream.GetData(8, idx)) || // SlopeAccumulationBullish !TempData.Add(m_ADWyckoffEventStream.GetData(9, idx)) || // SlopeAccumulationBearish !TempData.Add(m_ADWyckoffEventStream.GetData(10, idx)) || // SlopeDistributionBullish !TempData.Add(m_ADWyckoffEventStream.GetData(11, idx)) || // SlopeDistributionBearish !TempData.Add(m_ADWyckoffEventStream.GetData(12, idx)) || // Reaccumulation !TempData.Add(m_ADWyckoffEventStream.GetData(13, idx))) // Redistribution return false; } if(m_useADWyckoffFailedStructure) { if(ADIndicatorCold(m_ADWyckoffFailedStructure, "ADWyckoffFailedStructure")) // see the CumulativeDelta block's comment return false; // buffers: 0=Value, 1=BullishStructuralFailure, 2=BearishStructuralFailure, 3=FailedAccumulation, 4=FailedDistribution if(!TempData.Add(m_ADWyckoffFailedStructure.GetData(0, idx)) || // Value !TempData.Add(m_ADWyckoffFailedStructure.GetData(1, idx)) || // BullishStructuralFailure !TempData.Add(m_ADWyckoffFailedStructure.GetData(2, idx)) || // BearishStructuralFailure !TempData.Add(m_ADWyckoffFailedStructure.GetData(3, idx)) || // FailedAccumulation !TempData.Add(m_ADWyckoffFailedStructure.GetData(4, idx))) // FailedDistribution return false; } if(m_useADWyckoffSignificantBarInversion) { if(ADIndicatorCold(m_ADWyckoffSignificantBarInversion, "ADWyckoffSignificantBarInversion")) // see the CumulativeDelta block's comment return false; // buffers: 0=SignificantBarQuality, 1=BullishSignificantBar, 2=BearishSignificantBar, 3=BullishControlFlip, 4=BearishControlFlip if(!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(0, idx)) || // SignificantBarQuality !TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(1, idx)) || // BullishSignificantBar !TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(2, idx)) || // BearishSignificantBar !TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(3, idx)) || // BullishControlFlip !TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(4, idx))) // BearishControlFlip return false; } if(m_useAltData) { //--- External publication-stamped block (COT/VIX/macro) - see System\AltData.mqh and the //--- matching m_neuronsCount block in Topology.mqh. As-of lookup by THIS bar's open time, so //--- a bar can only read values the live run would have had. Gaps 0-fill like cross-asset: //--- the block is additive context and "no reading yet" (pre-history warm-up, collector //--- outage, pinned column gone from the file) must degrade, never reject the bar. m_Time is //--- unconditional (see InitTime), so GetData here is safe on every path that reaches this. double av[]; m_altData.Features((datetime)m_Time.GetData(idx), av); int an = m_altData.FeatureCount(); for(int k = 0; k < an; k++) if(!TempData.Add(av[k])) return false; } //--- ONE finiteness/plausibility gate for the whole bar, rather than 60-odd individually guarded //--- Add() calls. Most blocks above already clamp their own output; the AD/Wyckoff blocks //--- deliberately do not, because those indicators emit plain readings with no natural range. But //--- MQL5's CDoubleBuffer::At() returns EMPTY_VALUE (DBL_MAX) for any index it holds no data for, //--- and (EMPTY_VALUE - close) / atr is ~1e307: still FINITE, so it sails through every downstream //--- isfinite() check, and still large enough to overflow the first batch-norm layer's running //--- variance and latch that layer to NaN permanently (see NormalizeHost in AI\NeuronBatchNorm.mqh - //--- that is the 2026-08-02 "BufferWrite failed for buffer 3" run). Neutral-fill rather than reject //--- the bar: "this indicator has no reading here" is the degraded-but-usable case that the swing, //--- cross-asset and spread blocks above all already handle the same way. int featureEnd = TempData.Total(); for(int f = featureStart; f < featureEnd; f++) { double v = TempData.At(f); if((!MathIsValidNumber(v) || MathAbs(v) > FEATURE_ABS_MAX) && !TempData.Update(f, 0.0)) return false; } return true; } //+------------------------------------------------------------------+ //| "Is this AD indicator still calculating?" MT5 fills custom- | //| indicator buffers asynchronously after the handle is created, and | //| a cold one returns EMPTY_VALUE for EVERY index - including the | //| newest bar, which a warm indicator always has. So probing buffer | //| 0 at index 0 cleanly separates "async calc hasn't run yet" (cold: | //| transient reject, retried next call like the cold-ATR guard) from | //| "this deep bar is beyond the buffered depth" (warm: neutral-fill | //| by the sanitize loop, since that history is degraded-but-usable). | //| Cost is one array read per block per bar. (2026-08-11) | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ADIndicatorCold(CiCustom &ind, string block) { if(ind.GetData(0, 0) != EMPTY_VALUE) return false; m_featureFailTransient = true; m_featureFailBlock = StringFormat("%s - COLD (newest bar EMPTY, whole buffer unreadable)," " BarsCalculated=%d", block, ind.BarsCalculated()); return true; } //+------------------------------------------------------------------+ //| Initialize Open indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitOpen(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_Open))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_Open.Create(m_symbol.Name(), m_period)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize Close indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitClose(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_Close))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_Close.Create(m_symbol.Name(), m_period)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize High indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitHigh(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_High))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_High.Create(m_symbol.Name(), m_period)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize Low indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitLow(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_Low))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_Low.Create(m_symbol.Name(), m_period)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize Time indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitTime(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_Time))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_Time.Create(m_symbol.Name(), m_period)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize Volumes indicators. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitVolumes(CIndicators * indicators) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(!indicators.Add(GetPointer(m_Volumes))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object if(!m_Volumes.Create(m_symbol.Name(), m_period, VolumeData)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize MA indicator (feature use - see m_useMA). Period comes | //| from m_indicatorTuner.maPeriod, not the raw PeriodMA input - it | //| starts equal to it (see CADIndicatorTuner's constructor) but may | //| diverge once AutoTuneIndicators actually searches a trial. The | //| Classic Signals MA vote is unaffected - see m_useMA's declaration | //| comment. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitMA(CIndicators * indicators, bool addToCollection) { if(indicators == NULL) return (false); if(addToCollection && !indicators.Add(GetPointer(m_MA))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- unified MA custom indicator (CustomIndicators\ADMovingAverage.mq5); type AND period are both tuner- //--- driven (m_indicatorTuner.maType/maPeriod). params[1..] mirror the indicator's own input order. MqlParam params[9]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADMovingAverage"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.maType; // InpType params[2].type = TYPE_INT; params[2].integer_value = m_indicatorTuner.maPeriod; // InpPeriod params[3].type = TYPE_INT; params[3].integer_value = PRICE_CLOSE; // InpAppliedPrice params[4].type = TYPE_DOUBLE; params[4].double_value = 0.85; // InpOffset (ALMA) params[5].type = TYPE_DOUBLE; params[5].double_value = 6.0; // InpSigma (ALMA) params[6].type = TYPE_DOUBLE; params[6].double_value = 0.7; // InpVolumeFactor (T3) params[7].type = TYPE_DOUBLE; params[7].double_value = 0.001; // InpProcessNoise (Kalman) params[8].type = TYPE_DOUBLE; params[8].double_value = 0.1; // InpMeasurementNoise (Kalman) if(!m_MA.Create(m_symbol.Name(), m_period, IND_CUSTOM, 9, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_MA.NumBuffers(1); return (true); } //+------------------------------------------------------------------+ //| Initialize RSI indicator (feature use - see m_useRSI). Period | //| comes from m_indicatorTuner.rsiPeriod - see InitMA()'s comment. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitRSI(CIndicators * indicators, bool addToCollection) { if(indicators == NULL) return (false); if(addToCollection && !indicators.Add(GetPointer(m_RSI))) { printf(__FUNCTION__ + ": error adding object"); return (false); } if(!m_RSI.Create(m_symbol.Name(), m_period, m_indicatorTuner.rsiPeriod, PRICE_CLOSE)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } return (true); } //+------------------------------------------------------------------+ //| Initialize MACD indicator (feature use - see m_useMACD). Periods | //| come from m_indicatorTuner.macdFast/macdSlow/macdSignal - see | //| InitMA()'s comment for the "starts at the input, may diverge once | //| the tuner searches" split, and note the Classic Signals MACD vote | //| (Signals\SignalMACD.mqh) keeps its own separate instance. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitMACDFeature(CIndicators * indicators, bool addToCollection) { if(indicators == NULL) return (false); if(addToCollection && !indicators.Add(GetPointer(m_MACDFeature))) { printf(__FUNCTION__ + ": error adding object"); return (false); } if(!m_MACDFeature.Create(m_symbol.Name(), m_period, m_indicatorTuner.macdFast, m_indicatorTuner.macdSlow, m_indicatorTuner.macdSignal, PRICE_CLOSE)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } return (true); } //+------------------------------------------------------------------+ //| Initialize Ichimoku indicator (feature use - see m_useIchimoku). | //| Periods come from m_indicatorTuner.ichiTenkan/ichiKijun/ | //| ichiSenkou - see InitMA()'s comment. The Classic Signals Ichimoku | //| vote (Signals\SignalIchimoku.mqh) keeps its own instance. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitIchimoku(CIndicators * indicators, bool addToCollection) { if(indicators == NULL) return (false); if(addToCollection && !indicators.Add(GetPointer(m_Ichimoku))) { printf(__FUNCTION__ + ": error adding object"); return (false); } if(!m_Ichimoku.Create(m_symbol.Name(), m_period, m_indicatorTuner.ichiTenkan, m_indicatorTuner.ichiKijun, m_indicatorTuner.ichiSenkou)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } return (true); } //+------------------------------------------------------------------+ //| Initialize AD Cumulative Delta (CustomIndicators\ADCumulativeDelta.mq5) | //| Loaded via iCustom/CiCustom, not a built-in Ci* class - the compiled | //| indicator must be present under MQL5\Indicators\ (see | //| ExtractCustomIndicators() in Warrior_EA.mq5). Uses the indicator's | //| own input defaults; 6 output buffers, one TempData feature each. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADCumulativeDelta(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADCumulativeDelta))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADCumulativeDelta.mq5's own input order exactly MqlParam params[11]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADCumulativeDelta"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.adCumDelta.lookback; // InpLookbackPeriod params[2].type = TYPE_DOUBLE; params[2].double_value = m_indicatorTuner.adCumDelta.volClimax; // InpVolumeClimaxMultiplier params[3].type = TYPE_DOUBLE; params[3].double_value = m_indicatorTuner.adCumDelta.volHigh; // InpVolumeHighMultiplier params[4].type = TYPE_DOUBLE; params[4].double_value = m_indicatorTuner.adCumDelta.rangeClimax; // InpRangeClimaxMultiplier params[5].type = TYPE_DOUBLE; params[5].double_value = m_indicatorTuner.adCumDelta.rangeSignificant; // InpRangeSignificantMult params[6].type = TYPE_DOUBLE; params[6].double_value = m_indicatorTuner.adCumDelta.stVolRatio; // InpSTVolumeRatio params[7].type = TYPE_DOUBLE; params[7].double_value = m_indicatorTuner.adCumDelta.atrMult; // InpATRMultiplier params[8].type = TYPE_INT; params[8].integer_value = 0; // InpContextMode - DO NOT tune params[9].type = TYPE_INT; params[9].integer_value = 5; // InpSessionType - DO NOT tune params[10].type = TYPE_INT; params[10].integer_value = 1; // InpSessionCount - DO NOT tune if(!m_ADCumulativeDelta.Create(m_symbol.Name(), m_period, IND_CUSTOM, 11, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_ADCumulativeDelta.NumBuffers(6); //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize AD Shortening of Thrust (CustomIndicators\ADShorteningOfThrust.mq5) | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADShorteningOfThrust(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADShorteningOfThrust))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADShorteningOfThrust.mq5's own input order exactly MqlParam params[7]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADShorteningOfThrust"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.adSOT.thrustLookback; // InpThrustLookback params[2].type = TYPE_INT; params[2].integer_value = m_indicatorTuner.adSOT.minImpulses; // InpMinImpulses params[3].type = TYPE_DOUBLE; params[3].double_value = m_indicatorTuner.adSOT.sotThreshold; // InpSOTThreshold params[4].type = TYPE_INT; params[4].integer_value = 0; // InpContextMode - DO NOT tune params[5].type = TYPE_INT; params[5].integer_value = 5; // InpSessionType - DO NOT tune params[6].type = TYPE_INT; params[6].integer_value = 1; // InpSessionCount - DO NOT tune if(!m_ADShorteningOfThrust.Create(m_symbol.Name(), m_period, IND_CUSTOM, 7, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_ADShorteningOfThrust.NumBuffers(4); //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize AD Wyckoff Event Stream (CustomIndicators\ADWyckoffEventStream.mq5) | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADWyckoffEventStream(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffEventStream))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADWyckoffEventStream.mq5's own input order exactly. //--- NOTE the order is NOT grouped by meaning: the three range-lifecycle knobs the indicator gained on //--- 2026-08-02 were appended AFTER the session inputs, not next to the thresholds they belong with. //--- MqlParam is positional, so this list follows the indicator's declaration order, not a tidier one. MqlParam params[17]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADWyckoffEventStream"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.adWES.lookback; // InpLookback params[2].type = TYPE_INT; params[2].integer_value = m_indicatorTuner.adWES.zigzag; // InpZigZag params[3].type = TYPE_DOUBLE; params[3].double_value = m_indicatorTuner.adWES.volClimax; // InpVolClimax params[4].type = TYPE_DOUBLE; params[4].double_value = m_indicatorTuner.adWES.volHigh; // InpVolHigh params[5].type = TYPE_DOUBLE; params[5].double_value = m_indicatorTuner.adWES.rangeClimax; // InpRangeClimax params[6].type = TYPE_DOUBLE; params[6].double_value = m_indicatorTuner.adWES.rangeSignificant; // InpRangeSignificant params[7].type = TYPE_DOUBLE; params[7].double_value = m_indicatorTuner.adWES.stVolRatio; // InpSTVolRatio params[8].type = TYPE_DOUBLE; params[8].double_value = m_indicatorTuner.adWES.atr; // InpATR params[9].type = TYPE_INT; params[9].integer_value = 0; // InpContextMode - DO NOT tune params[10].type = TYPE_INT; params[10].integer_value = 5; // InpSessionType - DO NOT tune params[11].type = TYPE_INT; params[11].integer_value = 1; // InpSessionCount - DO NOT tune params[12].type = TYPE_DOUBLE; params[12].double_value = m_indicatorTuner.adWES.touchATR; // InpTouchATR params[13].type = TYPE_DOUBLE; params[13].double_value = m_indicatorTuner.adWES.arMinATR; // InpARMinATR params[14].type = TYPE_INT; params[14].integer_value = m_indicatorTuner.adWES.maxRangeBars; // InpMaxRangeBars //--- InpShowLabels/InpShowZones - forced OFF, and deliberately NOT tunable. This handle exists purely //--- to read buffers as network features; it is never the user's chart indicator. Left at their own //--- `true` defaults the indicator would litter the traded chart with AWY_-prefixed labels and range //--- rectangles that the EA does not own and its OnDeinit chart sweep does not know to remove. params[15].type = TYPE_BOOL; params[15].integer_value = 0; // InpShowLabels params[16].type = TYPE_BOOL; params[16].integer_value = 0; // InpShowZones if(!m_ADWyckoffEventStream.Create(m_symbol.Name(), m_period, IND_CUSTOM, 17, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_ADWyckoffEventStream.NumBuffers(14); //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize AD Wyckoff Failed Structure (CustomIndicators\ADWyckoffFailedStructure.mq5) | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADWyckoffFailedStructure(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffFailedStructure))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADWyckoffFailedStructure.mq5's own input order exactly MqlParam params[12]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADWyckoffFailedStructure"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.adWFS.lookback; // InpLookbackPeriod params[2].type = TYPE_INT; params[2].integer_value = m_indicatorTuner.adWFS.zigzagStrength; // InpZigZagStrength params[3].type = TYPE_DOUBLE; params[3].double_value = m_indicatorTuner.adWFS.volClimax; // InpVolumeClimaxMultiplier params[4].type = TYPE_DOUBLE; params[4].double_value = m_indicatorTuner.adWFS.volHigh; // InpVolumeHighMultiplier params[5].type = TYPE_DOUBLE; params[5].double_value = m_indicatorTuner.adWFS.rangeClimax; // InpRangeClimaxMultiplier params[6].type = TYPE_DOUBLE; params[6].double_value = m_indicatorTuner.adWFS.rangeSignificant; // InpRangeSignificantMult params[7].type = TYPE_DOUBLE; params[7].double_value = m_indicatorTuner.adWFS.stVolRatio; // InpSTVolumeRatio params[8].type = TYPE_DOUBLE; params[8].double_value = m_indicatorTuner.adWFS.atrMult; // InpATRMultiplier params[9].type = TYPE_INT; params[9].integer_value = 0; // InpContextMode - DO NOT tune params[10].type = TYPE_INT; params[10].integer_value = 5; // InpSessionType - DO NOT tune params[11].type = TYPE_INT; params[11].integer_value = 1; // InpSessionCount - DO NOT tune if(!m_ADWyckoffFailedStructure.Create(m_symbol.Name(), m_period, IND_CUSTOM, 12, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_ADWyckoffFailedStructure.NumBuffers(5); //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize AD Wyckoff Significant Bar Inversion (CustomIndicators\ADWyckoffSignificantBarInversion.mq5) | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADWyckoffSignificantBarInversion(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffSignificantBarInversion))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADWyckoffSignificantBarInversion.mq5's own input order exactly MqlParam params[8]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADWyckoffSignificantBarInversion"); params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.adWSBI.lookback; // InpLookback params[2].type = TYPE_DOUBLE; params[2].double_value = m_indicatorTuner.adWSBI.rangeSignificant; // InpRangeSignificant params[3].type = TYPE_DOUBLE; params[3].double_value = m_indicatorTuner.adWSBI.volumeHigh; // InpVolumeHigh params[4].type = TYPE_DOUBLE; params[4].double_value = m_indicatorTuner.adWSBI.atr; // InpATR params[5].type = TYPE_INT; params[5].integer_value = 0; // InpContextMode - DO NOT tune params[6].type = TYPE_INT; params[6].integer_value = 5; // InpSessionType - DO NOT tune params[7].type = TYPE_INT; params[7].integer_value = 1; // InpSessionCount - DO NOT tune if(!m_ADWyckoffSignificantBarInversion.Create(m_symbol.Name(), m_period, IND_CUSTOM, 8, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } m_ADWyckoffSignificantBarInversion.NumBuffers(5); //--- ok return (true); } //+------------------------------------------------------------------+ //| Initialize AD ZigZag (CustomIndicators\ADZigZag.mq5) - the | //| training-label source (see m_ADZigZag's declaration comment). | //| Always run at its own stock defaults (Depth=12, Deviation=5, | //| Backstep=3) - unlike the AD* feature indicators above, this has | //| no tunable-param struct and is never touched by AutoTuneIndicators.| //+------------------------------------------------------------------+ bool CExpertSignalAIBase::InitADZigZag(CIndicators * indicators, bool addToCollection) { //--- check pointer if(indicators == NULL) return (false); //--- add object to collection if(addToCollection && !indicators.Add(GetPointer(m_ADZigZag))) { printf(__FUNCTION__ + ": error adding object"); return (false); } //--- initialize object; params[1..] mirror ADZigZag.mq5's own input order exactly - stock defaults, //--- intentionally not sourced from a tunable params struct (see this function's declaration comment) MqlParam params[4]; params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADZigZag"); params[1].type = TYPE_INT; params[1].integer_value = 12; // InpDepth params[2].type = TYPE_INT; params[2].integer_value = 5; // InpDeviation params[3].type = TYPE_INT; params[3].integer_value = 3; // InpBackstep if(!m_ADZigZag.Create(m_symbol.Name(), m_period, IND_CUSTOM, 4, params)) { printf(__FUNCTION__ + ": error initializing object"); return (false); } // Must match ADZigZag.mq5's #property indicator_buffers exactly (3: main ZigZag buffer + 2 // internal INDICATOR_CALCULATIONS buffers), even though only buffer 0 is ever read via // GetData() - see the working AD Wyckoff indicators' InitAD*() for the same pattern. m_ADZigZag.NumBuffers(3); //--- ok return (true); } #endif // WARRIOR_AIBASE_FEATURES_MQH