//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Live continual learning, the EMA shadow net, and the OOS continu| //| | //| 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_ONLINELEARNING_MQH #define WARRIOR_AIBASE_ONLINELEARNING_MQH //+------------------------------------------------------------------+ //| Clones the just-converged Net into a separate CNet (m_simOosNet) | //| and arms a chunked bar-by-bar walk through the OOS window - see | //| AdvanceOosSimulationChunk(). Evaluation-only: the clone's learned | //| weights are never written back to Net or any persisted file. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::StartOosContinualSimulation(int bars, int oosCutoff) { if(m_simOosRunActive) { delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; } if(oosCutoff <= 0) return; // nothing to walk this run //--- Clone via the full Save()/Load() pair. Load() calls InitOpenCL()/InitDirectML() before //--- reconstructing layers, so a bare "new CNet(NULL)" ends up with a GPU/DirectML backend matching //--- production. Any lighter-weight restore that reused the CALLER's opencl/directml pointers would //--- be wrong here: on a fresh CNet(NULL) (whose constructor no-ops for a NULL description) those are //--- unset, and the clone would come out degenerate. (A file-based checkpoint pair used to sit beside //--- Save/Load and had exactly that flaw; it has been removed - the in-run snapshot is now the //--- in-memory CNet::CaptureWeights/RestoreWeights.) //--- Co-locate this ephemeral clone temp with the active model (COMMON on a live chart, LOCAL in the //--- tester sandbox) instead of always LOCAL. Uses m_activeFileName for the same reason (the active //--- model's base name, whichever context we're in). string simFile = m_activeFileName + "_simoos.tmp"; int simFlags = m_activeFileCommon ? FILE_COMMON : 0; double ip[]; if(!Net.Save(simFile, 0.0, 0.0, 0.0, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, ip)) return; m_simOosNet = new CNet(NULL); double loadE, loadU, loadF; datetime loadTime; long loadEra; bool loadComplete; double loadIp[]; bool loaded = m_simOosNet.Load(simFile, loadE, loadU, loadF, loadTime, m_activeFileCommon, loadEra, loadComplete, loadIp, true /*quiet: this evaluation-only sim is optional - on a miss it simply doesn't run*/); FileDelete(simFile, simFlags); if(!loaded) { delete m_simOosNet; m_simOosNet = NULL; return; } m_simOosCutoff = oosCutoff; m_simOosBarIndex = oosCutoff - 1; m_simOosForecast = 0; m_simOosSamples = 0; m_simOosRunActive = true; } //+------------------------------------------------------------------+ //| Advances the evaluation-only continual-learning OOS walk by up | //| to TRAIN_TIME_BUDGET_MS of work, then yields (same chunking | //| pattern as the real era loop's m_eraResumePending). For each bar, | //| oldest-OOS to newest: predict with the clone's CURRENT weights, | //| score against the cached true label, THEN let the clone learn | //| from it (single pass, no oversampling replay) - simulating how | //| the model would adapt bar-by-bar in real forward trading. Never | //| touches Net, never Saves the clone - purely an evaluation metric.| //+------------------------------------------------------------------+ void CExpertSignalAIBase::AdvanceOosSimulationChunk(void) { const uint SIM_TIME_BUDGET_MS = 80; uint chunkStartTick = GetTickCount(); //--- Mirror OnlineLearnStep()'s pinned rate for the duration of this chunk, and hand the shared //--- global back on BOTH exit paths - this simulation is only a valid forecast of live continual //--- learning if it steps at the same size, and `eta` is shared by every signal instance. double savedEta = eta; eta = m_modelEta * ONLINE_LEARN_ETA_SCALE; int i; for(i = m_simOosBarIndex; i >= 0; i--) { if(GetTickCount() - chunkStartTick >= SIM_TIME_BUDGET_MS) { m_simOosBarIndex = i; eta = savedEta; return; } if(i >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[i]) continue; // no cached label for this bar (e.g. right at a window edge) - nothing to learn from TempData.Clear(); TempData.Reserve((int)m_historyBars * m_neuronsCount); //--- Window ends AT (includes) bar i - see Train()'s matching r declaration comment for why. int r = i; bool bufferOk = true; for(int b = 0; b < (int)m_historyBars; b++) { if(!BufferTempData(r + b)) { bufferOk = false; break; } } if(!bufferOk || TempData.Total() < (int)m_historyBars * m_neuronsCount) continue; m_simOosNet.feedForward(TempData); m_simOosNet.getResults(TempData); double simSignal = (m_outputNeuronsCount == 3) ? ApplyClassificationSoftmax() : TempData[0]; //--- Pre-update softmax probabilities, read before TempData is rebuilt as the target vector - //--- feeds the same alpha-balanced focal weight the live path applies (OnlineSampleWeight). double sBuy = (TempData.Total() > 0) ? TempData.At(0) : 0.0; double sSell = (TempData.Total() > 1) ? TempData.At(1) : 0.0; double sNeutral = (TempData.Total() > 2) ? TempData.At(2) : 0.0; bool buy = m_labelCacheBuy[i]; bool sell = m_labelCacheSell[i]; ENUM_SIGNAL trueSignal = buy ? Buy : (sell ? Sell : Neutral); bool hit = (DoubleToSignal(simSignal) == trueSignal); m_simOosSamples++; if(hit) m_simOosForecast += (100 - m_simOosForecast) / Net.recentAverageSmoothingFactor; else m_simOosForecast -= m_simOosForecast / Net.recentAverageSmoothingFactor; TempData.Clear(); if(m_outputNeuronsCount == 1) TempData.Add(buy && !sell ? 1 : !buy && sell ? -1 : 0); else if(m_outputNeuronsCount == 3) { TempData.Add(buy ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); TempData.Add(sell ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); TempData.Add((!buy && !sell) ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); } m_simOosNet.backProp(TempData, OnlineSampleWeight(trueSignal, sBuy, sSell, sNeutral)); } eta = savedEta; delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; Print(ID + ": continual-learning OOS simulation complete - " + IntegerToString(m_simOosSamples) + " samples, accuracy " + DoubleToString(m_simOosForecast, 1) + "%"); } //+------------------------------------------------------------------+ //| Alpha-balanced focal weight for one streamed bar - the cost-level | //| imbalance correction used by BOTH the live continual-learning path | //| and its OOS simulation. See the declaration comment and the | //| ONLINE_LEARN_* block's CLASS IMBALANCE note for the derivation. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::OnlineSampleWeight(ENUM_SIGNAL trueSignal, double pBuy, double pSell, double pNeutral) { //--- Regression head has no class structure to balance. if(m_outputNeuronsCount != 3) return 1.0; double weight = 1.0; //--- alpha_c: inverse class frequency from the measured, persisted priors, normalised so the //--- MAJORITY class is exactly 1.0 (a majority bar is never down-weighted below parity) and only //--- minority bars are ever up-weighted. Unmeasured priors - a model deployed before any prior was //--- recorded - skip the alpha term rather than divide by zero; focal's (1-p_t)^gamma still applies. double priorMax = MathMax(m_priorNeutral, MathMax(m_priorBuy, m_priorSell)); bool priorsUsable = (priorMax > 0.0 && m_priorBuy > 0.0 && m_priorSell > 0.0 && m_priorNeutral > 0.0); if(priorsUsable && trueSignal != Neutral) { double truePrior = (trueSignal == Buy) ? m_priorBuy : m_priorSell; //--- Same shape as Train()'s repCount: measured ratio dialled by the OversampleParity input, so //--- one knob governs both engines. Capped so a single rare bar can never deliver an outsized //--- kick to an already-validated deployed model; ConstrainReplay tightens the cap to 3.0, the //--- effective correction Train() applies under that same input. double alphaCap = m_constrainReplay ? 3.0 : ONLINE_LEARN_MAX_CLASS_WEIGHT; weight = MathMin(MathMax(1.0, (priorMax / truePrior) * m_oversampleParity), alphaCap); } //--- gamma: the CONFIGURED m_focalGamma, never m_focalGammaRuntime - the runtime value is a training //--- schedule artifact the plateau ladder anneals toward 0 and it has no meaning post-deploy. //--- Down-weights bars the model already gets right (the overwhelming Neutral majority), so the //--- update concentrates on genuinely informative confirmations. if(m_focalGamma > 0.0) { double pt = (trueSignal == Buy) ? pBuy : (trueSignal == Sell) ? pSell : pNeutral; pt = MathMax(0.0, MathMin(1.0, pt)); weight *= MathPow(1.0 - pt, m_focalGamma); } return weight; } //+------------------------------------------------------------------+ //| Online continual-learning step - LIVE CHART ONLY. Once a model is | //| deployed (m_trainingComplete) it keeps learning from real market | //| structure the same supervised way it was trained: predicting the | //| ZigZag reversal label for each bar. The critical rule the user | //| asked for is the confirmation delay - a ZigZag pivot on a recent | //| bar REPAINTS until m_swingConfirmationBars more bars have closed | //| after it (see m_swingConfirmationBars / AdvanceZigZagLabelState), | //| so the model must NEVER backprop the newest bars against a still- | //| provisional label, even though it happily EMITS a live signal on | //| them. This method therefore only ever learns from the "confirmable | //| frontier" and older: the newest bar whose now-relative index is | //| >= m_swingConfirmationBars. Everything newer than that is inference-| //| only until it, too, matures - identical to how training holds its | //| recent bars in the OOS holdout and embargoes the boundary band. | //| | //| Mechanism per newly-matured bar (oldest->newest, exactly | //| AdvanceOosSimulationChunk()'s predict-score-then-learn step, but | //| on the REAL deployed Net): build the same feature window training | //| used, feedForward, score the prediction against the confirmed | //| label (guardrail EMA), then backProp that label. The deployed | //| SHADOW is nudged toward Net by SHADOW_WEIGHT_TAU only while the | //| rolling accuracy holds up; if it decays the blend FREEZES (live | //| keeps trading the last-good shadow, Net keeps adapting so it can | //| recover) - drift can never reach the account. State persists in the | //| .stats sidecar so a restart neither re-learns old bars nor skips. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::OnlineLearnStep(void) { //--- Hard gates. m_inferenceOnly covers BOTH the single backtest and every optimization pass (see //--- its declaration comment): in the tester the model is held FIXED, so continual learning is a //--- live-chart-only behaviour (forward-test it on a demo account, not the Strategy Tester). if(!m_enableOnlineLearning || m_inferenceOnly || m_evalMode || m_trainRunActive) return; if(!m_trainingComplete || m_trainingStopRequested || m_trainingPaused) return; if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD)) return; // belt-and-braces: never adapt weights inside any tester context if(CheckPointer(Net) == POINTER_INVALID || Net.CpuInference()) return; // DLL-free inference build has no backend to backprop through if(CheckPointer(m_shadowNet) == POINTER_INVALID) return; // nothing deployed to blend into yet (RefreshLatestSignal bootstraps it first) if(m_outputNeuronsCount != 1 && m_outputNeuronsCount != 3) return; int conf = MathMax(m_swingConfirmationBars, 1); int barsAvail = Bars(m_symbol.Name(), PERIOD_CURRENT); //--- Need the frontier bar (now-relative index conf) plus a full feature window BEHIND it, plus a //--- little slack so a short catch-up walk stays in-bounds. int need = conf + (int)m_historyBars + 2; if(barsAvail < need) return; //--- Load enough history for the frontier window and a bounded catch-up; RefreshConvergedSignal() //--- only sized buffers relative to dtStudied (newest bars), which is too shallow to reach the //--- confirmation frontier. int wantBars = MathMin(need + ONLINE_LEARN_MAX_CATCHUP, barsAvail); if(!ResizeBuffers(wantBars) || !RefreshData()) return; datetime frontierTime = m_Time.GetData(conf); if(frontierTime <= 0) return; //--- First step of this deployment (or a model that never online-learned): DON'T retroactively //--- backfill the whole history through backprop in one shot - that could shift the just-validated //--- deployed model materially before any live confirmation. Anchor the watermark at the current //--- frontier and begin learning from genuinely new confirmations forward. if(m_onlineLearnedUpToTime <= 0) { m_onlineLearnedUpToTime = frontierTime; return; } if(frontierTime <= m_onlineLearnedUpToTime) return; // no bar has matured past the watermark since last time //--- Seed the guardrail EMA from the model's deploy-time OOS accuracy the first time we actually //--- learn, so the floor is meaningful from the very first update (not a cold 0 that would trip it). if(m_onlineRollingAcc < 0.0) m_onlineRollingAcc = (dForecast > 0.0 && dForecast <= 100.0) ? dForecast : 100.0; //--- Guardrail floor: deploy baseline minus a margin, never below the absolute minimum. double baseline = (dForecast > 0.0 && dForecast <= 100.0) ? dForecast : 100.0; double accFloor = MathMax(ONLINE_LEARN_MIN_ACC, baseline - ONLINE_LEARN_ACC_MARGIN); //--- Find the oldest not-yet-learned confirmed bar: walk from the frontier (index conf) toward older //--- bars (increasing index) until we pass the watermark or hit the catch-up cap, then learn newest- //--- ward from there so bars are consumed in strict chronological (oldest->newest) order. int oldestIdx = conf; while(oldestIdx < barsAvail - 1 && oldestIdx < conf + ONLINE_LEARN_MAX_CATCHUP && m_Time.GetData(oldestIdx) > m_onlineLearnedUpToTime) oldestIdx++; //--- oldestIdx now points at the first bar whose time is <= watermark (already learned) or the cap; //--- the newest UNLEARNED bar is one step newer (idx-1). Learn from idx = oldestIdx-1 down to conf. //--- Pin the learning rate for the duration of this walk and restore it after: `eta` is a GLOBAL //--- shared by every signal instance, so leaving it modified would corrupt another model's training //--- chunk - see ONLINE_LEARN_ETA_SCALE's comment. double savedEta = eta; eta = m_modelEta * ONLINE_LEARN_ETA_SCALE; int learned = 0; for(int idx = oldestIdx - 1; idx >= conf; idx--) { datetime bt = m_Time.GetData(idx); if(bt <= m_onlineLearnedUpToTime) continue; // already learned (defensive; the walk above should exclude it) //--- Build this bar's feature window - IDENTICAL to Train()/RefreshLatestSignal(): ends AT bar idx //--- and extends m_historyBars into the past. No lookahead (all bars are older than idx). TempData.Clear(); TempData.Reserve((int)m_historyBars * m_neuronsCount); bool bufferOk = true; for(int b = 0; b < (int)m_historyBars; b++) if(!BufferTempData(idx + b)) { bufferOk = false; break; } if(!bufferOk || TempData.Total() < (int)m_historyBars * m_neuronsCount) { //--- window not buildable this bar (e.g. an indicator hole) - advance the watermark past it so //--- we don't wedge re-trying the same bar forever, but learn nothing from it. m_onlineLearnedUpToTime = bt; continue; } //--- Predict with the CURRENT (pre-update) weights, then score against the confirmed label for the //--- rolling guardrail - exactly AdvanceOosSimulationChunk()'s predict-before-learn measurement. Net.feedForward(TempData); Net.getResults(TempData); double predSignal = (m_outputNeuronsCount == 3) ? ApplyClassificationSoftmax() : TempData[0]; ENUM_SIGNAL trueSignal = ConfirmedZigZagLabel(idx); bool hit = (DoubleToSignal(predSignal) == trueSignal); m_onlineRollingAcc += (100.0 * (hit ? 1.0 : 0.0) - m_onlineRollingAcc) / ONLINE_ACC_SMOOTH; //--- Per-class softmax probabilities as of THIS bar's pre-update prediction. Must be read here, //--- before TempData is rebuilt as the target vector below - ApplyClassificationSoftmax() has //--- already normalised TempData[0..2] in place into a genuine distribution (same contract pass 2 //--- relies on for its own focal term). double pBuy = (TempData.Total() > 0) ? TempData.At(0) : 0.0; double pSell = (TempData.Total() > 1) ? TempData.At(1) : 0.0; double pNeutral = (TempData.Total() > 2) ? TempData.At(2) : 0.0; //--- Build the target vector - identical encoding to Train()/AdvanceOosSimulationChunk(). bool buy = (trueSignal == Buy); bool sell = (trueSignal == Sell); TempData.Clear(); if(m_outputNeuronsCount == 1) TempData.Add(buy && !sell ? 1 : (!buy && sell ? -1 : 0)); else { TempData.Add(buy ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); TempData.Add(sell ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); TempData.Add((!buy && !sell) ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); } Net.backProp(TempData, OnlineSampleWeight(trueSignal, pBuy, pSell, pNeutral)); m_onlineSamples++; //--- Deploy the improvement ONLY while accuracy holds. Warmup: allow the first few blends (the //--- model was just validated at deploy, steps are tiny) until the EMA has enough samples to judge. bool blendOk = (m_onlineSamples <= ONLINE_LEARN_WARMUP) || (m_onlineRollingAcc >= accFloor); if(blendOk) { m_shadowNet.BlendWeightsFrom(Net, SHADOW_WEIGHT_TAU); if(m_onlineBlendFrozen) { m_onlineBlendFrozen = false; Print(ID + ": online-learning deployment RESUMED - rolling accuracy recovered to " + DoubleToString(m_onlineRollingAcc, 1) + "% (floor " + DoubleToString(accFloor, 1) + "%)"); } } else if(!m_onlineBlendFrozen) { m_onlineBlendFrozen = true; Print(ID + ": online-learning deployment FROZEN - rolling accuracy " + DoubleToString(m_onlineRollingAcc, 1) + "% fell below floor " + DoubleToString(accFloor, 1) + "%; live keeps trading the last-good model while it adapts"); } m_onlineLearnedUpToTime = bt; learned++; m_onlineBarsSincePersist++; } //--- Hand the shared global back exactly as found, on BOTH exit paths below - see the matching //--- savedEta assignment above for why this must not leak out of this function. eta = savedEta; if(learned <= 0) return; //--- Periodic durable persistence so a crash loses at most ONLINE_LEARN_PERSIST_EVERY bars of //--- adaptation (shutdown also persists via PersistOnShutdown()). if(m_onlineBarsSincePersist >= ONLINE_LEARN_PERSIST_EVERY) { double ip[]; m_indicatorTuner.Flatten(ip); bool saveOk = Net.Save(m_activeFileName + ".nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, ip); if(!saveOk) Print(ID + ": ERROR - online-learning Net.Save failed for " + m_activeFileName + ".nnw. Retrying next persist interval instead of resetting the bars-since-persist counter."); SaveShadowNet(ip); if(!SaveModelStats(m_activeFileName, m_activeFileCommon)) Print(ID + ": ERROR - online-learning SaveModelStats failed for " + m_activeFileName + "."); // Only reset the counter on a successful weight save - resetting unconditionally on a // transient failure would silently double the effective data-loss window on the NEXT failure too. if(saveOk) { m_onlineBarsSincePersist = 0; PrintVerbose(ID + ": online-learning checkpoint saved (" + IntegerToString((int)m_onlineSamples) + " total updates, rolling acc " + DoubleToString(m_onlineRollingAcc, 1) + "%)"); } } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CExpertSignalAIBase::SaveShadowNet(const double &indicatorParams[]) { if(CheckPointer(m_shadowNet) == POINTER_INVALID) return; m_shadowNet.Save(m_activeFileName + "_shadow.nnw", dError, dUndefine, dForecast, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, indicatorParams); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CExpertSignalAIBase::EnsureShadowNet(void) { if(CheckPointer(m_shadowNet) != POINTER_INVALID) return; //--- Pure-MQL5 inference (DLL-free backtest): no era blending happens, so the shadow would just be a //--- copy of Net - and bootstrapping one via Save/Load would spin a compute backend up on the clone, //--- defeating the DLL-free goal. Skip it; RefreshLatestSignal() falls back to Net directly. if(CheckPointer(Net) != POINTER_INVALID && Net.CpuInference()) return; string shadowFile = m_activeFileName + "_shadow.nnw"; if(FileIsExist(shadowFile, m_activeFileCommon ? FILE_COMMON : 0)) { CNet *loaded = new CNet(NULL); if(CheckPointer(loaded) != POINTER_INVALID) { double loadE, loadU, loadF; datetime loadTime; long loadEra; bool loadComplete; double loadIp[]; if(loaded.Load(shadowFile, loadE, loadU, loadF, loadTime, m_activeFileCommon, loadEra, loadComplete, loadIp, true /*quiet: a miss just falls through to the clone bootstrap below*/)) { m_shadowNet = loaded; //--- This second CNet spins up its OWN compute backend, so on a fresh attach the log shows a //--- second backend-init block right after the main model's. Name it here (verbose) so it //--- reads as "the shadow net came up" rather than "the EA started twice". PrintVerbose(ID + ": EMA shadow net restored from " + shadowFile + " (its own network instance - hence a second compute-backend init)"); return; } delete loaded; } } //--- No compatible persisted shadow - bootstrap from Net's current weights. Clone via the full //--- Save()/Load() pair - see StartOosContinualSimulation()'s matching comment for why a lighter //--- restore is not safe here (it would need opencl/directml already initialized on the target //--- CNet, which a bare "new CNet(NULL)" does not have). if(CheckPointer(Net) == POINTER_INVALID) return; //--- Attempt the clone bootstrap at most once per topology (see m_shadowBootstrapAttempted). On the //--- tester's CPU-DLL fallback a second full-net clone can fail to load; retrying every bar would //--- rebuild the compute backend each tick and crawl. Falling back to Net is correct and lossless here. if(m_shadowBootstrapAttempted) return; m_shadowBootstrapAttempted = true; //--- Co-locate the ephemeral clone temp with the active model (COMMON on a live chart, LOCAL in the //--- tester sandbox) instead of always LOCAL. string cloneFile = m_activeFileName + "_shadowclone.tmp"; int cloneFlags = m_activeFileCommon ? FILE_COMMON : 0; double ip[]; if(!Net.Save(cloneFile, 0.0, 0.0, 0.0, dtStudied, m_activeFileCommon, m_eraCount, m_trainingComplete, ip)) return; CNet *clone = new CNet(NULL); if(CheckPointer(clone) == POINTER_INVALID) { FileDelete(cloneFile, cloneFlags); return; } double loadE, loadU, loadF; datetime loadTime; long loadEra; bool loadComplete; double loadIp[]; bool loaded = clone.Load(cloneFile, loadE, loadU, loadF, loadTime, m_activeFileCommon, loadEra, loadComplete, loadIp, true /*quiet: best-effort clone, the miss is handled gracefully below*/); FileDelete(cloneFile, cloneFlags); if(!loaded) { delete clone; //--- Best-effort: without a shadow, live signals read the main Net directly (RefreshLatestSignal's //--- deployNet fallback), which is correct and lossless - so one calm line, not an error. //--- This used to be described as EXPECTED on a CPU-DLL box ("can't allocate a 2nd net"). It is not: //--- the clone load was failing for the same reason the MAIN model load was - CLayer::CreateElement //--- had stopped overriding CArrayObj::CreateElement, so every CNet::Load failed at layer 0 //--- regardless of backend (see AI\Network.mqh). With that fixed this path should be rare; if it //--- shows up repeatedly, investigate rather than assume a hardware limit. PrintVerbose(ID + ": EMA shadow net could not be bootstrapped - live signals use the main model directly (lossless fallback)."); return; } m_shadowNet = clone; //--- See the matching note on the restore path above: a second CNet means a second compute-backend init //--- in the log, which is expected, not a duplicated EA. PrintVerbose(ID + ": EMA shadow net bootstrapped from the main model's current weights (its own network instance - hence a second compute-backend init)"); } #endif // WARRIOR_AIBASE_ONLINELEARNING_MQH