Warrior_EA/Expert/AIBase/OnlineLearning.mqh
AnimateDread 2de93539d4 refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.

Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:

  Training.mqh        1607  era loop, plateau ladder, checkpoint select, deploy
  Features.mqh        1093  indicator creation + per-bar input feature vector
  ChartUI.mqh          634  arrows, arrow persistence, status panel, cleanup
  Persistence.mqh      492  .stats/.cfg sidecars, CPU-inference validation, copy
  OnlineLearning.mqh   461  live continual learning, EMA shadow, OOS simulator
  Labels.mqh           309  ZigZag pivot labels, async label-cache prebuild
  AutoTune.mqh         275  genetic tuner (population, crossover, halving)
  Inference.mqh        235  softmax, prior calibration, class priors

  ExpertSignalAIBase.mqh  8216 -> 3131 (declaration + topology build only)

This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00

479 lines
27 KiB
MQL5

//+------------------------------------------------------------------+
//| 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