//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Live continual learning, the EMA shadow net, the OOS continual- | //| learning simulation and the pattern-database backfill walk. | //| STATEFUL, unlike CModelPersistence: the shadow net and every | //| walk's own resume state (m_simOos*/m_dbBackfill*/m_online*) are | //| genuinely exclusive to this collaborator - grep-verified against | //| the rest of Expert\ (Training.mqh/Topology.mqh/Lifecycle.mqh only | //| ever CHECKED or RESET this state at era/lifecycle boundaries, | //| never owned it), so it lives here as real members instead of on | //| the signal. Every method below is a pure relocation of | //| Expert\AIBase\OnlineLearning.mqh's original bodies - same order, | //| same conditionals, no logic changes. | //+------------------------------------------------------------------+ #ifndef WARRIOR_ONLINELEARNING_ONLINELEARNING_MQH #define WARRIOR_ONLINELEARNING_ONLINELEARNING_MQH class COnlineLearning { private: COnlineLearningView *m_view; // BORROWED - the signal owns the adapter, not the reverse //--- EMA "shadow" copy of Net, blended a SHADOW_WEIGHT_TAU step toward Net at the end of every //--- era rather than replaced. Live inference reads THIS, so any single era's raw weights - //--- including an Adam overshoot - can only nudge what is deployed, never overwrite it. CNet *m_shadowNet; //--- One-shot latch for the clone bootstrap. Cloning a second net can fail on the tester's CPU- //--- DLL fallback, and without this the retry would re-initialise the compute backend on EVERY bar. bool m_shadowBootstrapAttempted; //--- ONLINE CONTINUAL-LEARNING STATE (see OnlineLearnStep(); tunables at ONLINE_LEARN_*). The //--- watermark is a bar TIME, not a now-relative index, so it survives the per-bar index-frame shift. bool m_enableOnlineLearning; datetime m_onlineLearnedUpToTime; double m_onlineRollingAcc; long m_onlineSamples; int m_onlineBarsSincePersist; //--- Latched log state so the guardrail freeze/resume transition prints once per flip, not per bar. bool m_onlineBlendFrozen; //--- Evaluation-only continual-learning OOS simulation: once the core model converges, a CLONE //--- of its weights (never the production Net itself) walks forward through the OOS window bar- //--- by-bar, scoring each bar with its current weights THEN learning from it. CNet *m_simOosNet; // NULL when no simulation is active bool m_simOosRunActive; int m_simOosCutoff; // oosCutoff snapshot from the run that converged int m_simOosBarIndex; // resume point, m_simOosCutoff-1 down to 0 double m_simOosForecast; // smoothed accuracy - separate from dOosForecast int m_simOosSamples; //--- ONE-SHOT pattern-database backfill (user request 2026-08-16): "the DB needs to be filled //--- during training so I do not have to run a backtest before deploying to live trading". bool m_dbBackfillActive; bool m_dbBackfillDone; // one-shot per deployment - never re-armed by a later call int m_dbBackfillIndex; // resume point, descends to 2 (mirrors pass 3's m_oosScoreIndex) int m_dbBackfillStartIndex; int m_dbBackfillStopIndex; // inclusive floor - the ranking slice's newest bar int m_dbBackfillBars; int m_dbBackfillFired; // rows written, for the completion log line long m_dbBackfillEra; // era stamped into the .dbfill marker on completion public: COnlineLearning(void); ~COnlineLearning(void); void Bind(COnlineLearningView *view) { m_view = view; } //--- Alpha-balanced focal sample weight (Lin et al. 2017 eq. 5) for ONE streamed bar - see the //--- ONLINE_LEARN_* block's CLASS IMBALANCE comment for the derivation. Returns 1.0 for the //--- regression head (no class structure). double SampleWeight(const ENUM_SIGNAL trueSignal, const double pBuy, const double pSell, const double pNeutral); //--- Online continual-learning step (live chart only) - see the implementation comment and the //--- ONLINE_LEARN_* tunables. No-op in the tester/optimizer and while training is active. void OnlineLearnStep(void); //--- Lazily bootstraps m_shadowNet if it's still NULL: tries loading a persisted shadow file //--- first (continuity across EA restarts), falling back to cloning Net's current weights if no //--- compatible shadow file exists yet. void EnsureShadowNet(void); //--- Persists m_shadowNet alongside every Net.Save() call, using the same run metadata the //--- caller already computed for Net.Save() itself. void SaveShadowNet(const double &indicatorParams[]); //--- The deploy net live trading/inference reads: shadow-preferred, falling back to the main Net //--- if the shadow isn't bootstrapped yet. CNet *DeployNet(void); //--- EMA shadow-weight deployment step: blend the shadow a small step (SHADOW_WEIGHT_TAU) toward //--- Net's just-updated weights - called once per era, after EnsureShadowNet(). void BlendTowardNet(void); void StartOosContinualSimulation(const int bars, const int oosCutoff); void AdvanceOosSimulationChunk(void); void StartPatternDatabaseBackfill(const int bars, const int totalIter, const int oosCutoff); void AdvancePatternDatabaseBackfill(void); //--- CONSOLIDATED state queries/resets - one call site, not a field poked from three places (the //--- original had this exact abort triple duplicated in Training.mqh AND twice more in //--- ExpertSignalAIBase.mqh's own header; see project memory on N-loose-members-cleared-twice). bool SimRunActive(void) const { return m_simOosRunActive; } bool BackfillActive(void) const { return m_dbBackfillActive; } void AbortSimIfActive(void); //--- A fresh topology invalidates any existing shadow and the whole continual-learning history - //--- see Topology.mqh's call site for why. void ResetForFreshTopology(void); bool Enabled(void) const { return m_enableOnlineLearning; } void SetEnabled(const bool v) { m_enableOnlineLearning = v; } //--- Persisted (WST3) via CModelPersistence - see IPersistenceView.mqh's OnlineLearnedUpToTime()/ //--- OnlineRollingAcc()/OnlineSamples(), which now reach these through the signal's owning member. datetime LearnedUpToTime(void) const { return m_onlineLearnedUpToTime; } void SetLearnedUpToTime(const datetime v) { m_onlineLearnedUpToTime = v; } double RollingAcc(void) const { return m_onlineRollingAcc; } void SetRollingAcc(const double v) { m_onlineRollingAcc = v; } long Samples(void) const { return m_onlineSamples; } void SetSamples(const long v) { m_onlineSamples = v; } }; //+------------------------------------------------------------------+ //| Matches what Lifecycle.mqh's constructor-init-list/destructor | //| used to do for these fields before this extraction. | //+------------------------------------------------------------------+ COnlineLearning::COnlineLearning(void) : m_view(NULL), m_shadowNet(NULL), m_shadowBootstrapAttempted(false), m_enableOnlineLearning(true), m_onlineLearnedUpToTime(0), m_onlineRollingAcc(-1.0), m_onlineSamples(0), m_onlineBarsSincePersist(0), m_onlineBlendFrozen(false), m_simOosNet(NULL), m_simOosRunActive(false), m_simOosCutoff(0), m_simOosBarIndex(-1), m_simOosForecast(0), m_simOosSamples(0), m_dbBackfillActive(false), m_dbBackfillDone(false), m_dbBackfillIndex(0), m_dbBackfillStartIndex(0), m_dbBackfillStopIndex(2), m_dbBackfillBars(0), m_dbBackfillFired(0), m_dbBackfillEra(-1) { } COnlineLearning::~COnlineLearning(void) { if(CheckPointer(m_shadowNet) != POINTER_INVALID) delete m_shadowNet; if(CheckPointer(m_simOosNet) != POINTER_INVALID) delete m_simOosNet; } //+------------------------------------------------------------------+ double COnlineLearning::SampleWeight(const ENUM_SIGNAL trueSignal, const double pBuy, const double pSell, const double pNeutral) { //--- Regression head has no class structure to balance. if(m_view.OutputNeurons() != 3) return 1.0; double weight = 1.0; double priorBuy = m_view.PriorBuy(); double priorSell = m_view.PriorSell(); double priorNeutral = m_view.PriorNeutral(); //--- 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. double priorMax = MathMax(priorNeutral, MathMax(priorBuy, priorSell)); bool priorsUsable = (priorMax > 0.0 && priorBuy > 0.0 && priorSell > 0.0 && priorNeutral > 0.0); if(priorsUsable && trueSignal != Neutral) { double truePrior = (trueSignal == Buy) ? priorBuy : priorSell; //--- Measured ratio scaled by ONLINE_LEARN_PARITY, then capped so a single rare bar can never //--- deliver an outsized kick to an already-validated deployed model. weight = MathMin(MathMax(1.0, (priorMax / truePrior) * ONLINE_LEARN_PARITY), ONLINE_LEARN_ALPHA_CAP); } //--- gamma: down-weights bars the model already gets right (the overwhelming Neutral majority), so //--- the update concentrates on genuinely informative confirmations. Constant since 2026-07-31. if(ONLINE_LEARN_FOCAL_GAMMA > 0.0) { double pt = (trueSignal == Buy) ? pBuy : (trueSignal == Sell) ? pSell : pNeutral; pt = MathMax(0.0, MathMin(1.0, pt)); weight *= MathPow(1.0 - pt, ONLINE_LEARN_FOCAL_GAMMA); } return weight; } //+------------------------------------------------------------------+ void COnlineLearning::AbortSimIfActive(void) { if(!m_simOosRunActive) return; delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; } //+------------------------------------------------------------------+ void COnlineLearning::ResetForFreshTopology(void) { //--- A fresh topology invalidates any existing shadow - its weights, if any, are shaped for the //--- OLD Net and would either mismatch dimensionally or, worse, silently blend unrelated weight //--- spaces if the shape happens to coincide. if(CheckPointer(m_shadowNet) != POINTER_INVALID) { delete m_shadowNet; m_shadowNet = NULL; } //--- Let EnsureShadowNet() re-attempt the clone bootstrap once for this new topology - the old //--- shadow, and any prior failed-bootstrap verdict, no longer apply. m_shadowBootstrapAttempted = false; //--- A brand-new untrained net has NO online continual-learning history: reset the watermark/ //--- guardrail/counters so a fresh start or a ResetWeights()-then-retrain never resumes from a //--- superseded model's learned-up-to point or its stale rolling accuracy. m_onlineLearnedUpToTime = 0; m_onlineRollingAcc = -1.0; m_onlineSamples = 0; m_onlineBarsSincePersist = 0; m_onlineBlendFrozen = false; } //+------------------------------------------------------------------+ CNet *COnlineLearning::DeployNet(void) { //--- Live trading/inference reads the EMA shadow net, not Net directly - falls back to Net if the //--- shadow isn't bootstrapped yet (should only be momentarily, before EnsureShadowNet() has run). return (CheckPointer(m_shadowNet) != POINTER_INVALID) ? m_shadowNet : m_view.NetPtr(); } //+------------------------------------------------------------------+ void COnlineLearning::BlendTowardNet(void) { if(CheckPointer(m_shadowNet) != POINTER_INVALID) m_shadowNet.BlendWeightsFrom(m_view.NetPtr(), SHADOW_WEIGHT_TAU); } //+------------------------------------------------------------------+ //| 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 COnlineLearning::StartOosContinualSimulation(const int bars, const int oosCutoff) { AbortSimIfActive(); if(oosCutoff <= 0) return; // nothing to walk this run //--- Clone via the full Save()/Load() pair. Load() calls InitOpenCL()/InitComputeDll() before //--- reconstructing layers, so a bare "new CNet(NULL)" ends up with a backend matching production. string simFile = m_view.ActiveFileName() + "_simoos.tmp"; int simFlags = m_view.ActiveFileCommon() ? FILE_COMMON : 0; double ip[]; CNet *net = m_view.NetPtr(); if(!net.Save(simFile, 0.0, 0.0, 0.0, m_view.StudiedTime(), m_view.ActiveFileCommon(), m_view.EraCount(), m_view.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_view.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). | //+------------------------------------------------------------------+ void COnlineLearning::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 `g_eta` is shared by every signal instance. double savedEta = g_eta; g_eta = m_view.ModelEta() * ONLINE_LEARN_ETA_SCALE; CArrayDouble *td = m_view.TempData(); CNet *net = m_view.NetPtr(); int i; for(i = m_simOosBarIndex; i >= 0; i--) { if(GetTickCount() - chunkStartTick >= SIM_TIME_BUDGET_MS) { m_simOosBarIndex = i; g_eta = savedEta; return; } if(!m_view.HasLabel(i)) continue; // no cached label for this bar (e.g. right at a window edge) - nothing to learn from //--- Window ends AT (includes) bar i - see Train()'s matching r declaration comment for why. int r = i; if(!m_view.BuildFeatureWindow(r)) continue; m_simOosNet.feedForward(td); m_simOosNet.getResults(td); double simSignal = (m_view.OutputNeurons() == 3) ? m_view.ApplyClassificationSoftmax() : td[0]; //--- Pre-update softmax probabilities, read before td is rebuilt as the target vector - feeds //--- the same alpha-balanced focal weight the live path applies (SampleWeight). double sBuy = (td.Total() > 0) ? td.At(0) : 0.0; double sSell = (td.Total() > 1) ? td.At(1) : 0.0; double sNeutral = (td.Total() > 2) ? td.At(2) : 0.0; bool buy = m_view.IsBuyLabel(i); bool sell = m_view.IsSellLabel(i); ENUM_SIGNAL trueSignal = buy ? Buy : (sell ? Sell : Neutral); bool hit = (m_view.SignalFromValue(simSignal) == trueSignal); m_simOosSamples++; if(hit) m_simOosForecast += (100 - m_simOosForecast) / net.recentAverageSmoothingFactor; else m_simOosForecast -= m_simOosForecast / net.recentAverageSmoothingFactor; td.Clear(); if(m_view.OutputNeurons() == 1) td.Add(buy && !sell ? 1 : !buy && sell ? -1 : 0); else if(m_view.OutputNeurons() == 3) { td.Add(buy ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); td.Add(sell ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); td.Add((!buy && !sell) ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); } m_simOosNet.backProp(td, SampleWeight(trueSignal, sBuy, sSell, sNeutral)); } g_eta = savedEta; delete m_simOosNet; m_simOosNet = NULL; m_simOosRunActive = false; Print(m_view.Id() + ": continual-learning OOS simulation complete - " + IntegerToString(m_simOosSamples) + " samples, accuracy " + DoubleToString(m_simOosForecast, 1) + "%"); } //+------------------------------------------------------------------+ //| Arms the one-shot pattern-database backfill (see the declaration | //| comment). Called right after FinalizeTrainRun() has restored the | //| DEPLOYED checkpoint, so the walk below scores with the exact | //| weights that are about to trade live - not the last era's, which | //| the plateau ladder may have superseded. | //+------------------------------------------------------------------+ void COnlineLearning::StartPatternDatabaseBackfill(const int bars, const int totalIter, const int oosCutoff) { if(m_dbBackfillDone || m_dbBackfillActive) return; if(!UseDatabaseRanking || MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD)) return; CNet *net = m_view.NetPtr(); if(m_view.FilterId() == "NULL" || oosCutoff <= 0 || CheckPointer(net) == POINTER_INVALID) return; //--- READS THE CALIBRATION BAND - never the window pass 3 grades. int calibLo = m_view.CalibLoIndex(oosCutoff); int calibHi = m_view.CalibHiIndex(totalIter, oosCutoff); if(m_view.CalibBandBars(totalIter, oosCutoff) <= 0 || calibHi <= calibLo) { m_dbBackfillDone = true; Print(m_view.Id() + ": pattern-database backfill SKIPPED - this era carved no calibration band (study" " window too short for OOS + two " + IntegerToString(m_view.CalibPurgeBars()) + "-bar purges + a" " band). Filter weights will build from real fills instead. Lengthen the study period or" " lower the OOS split % to enable it."); return; } //--- ONE-SHOT ACROSS ATTACHES, not merely across this object's lifetime - see the marker-file logic //--- below and the original declaration comment for the full duplicate-row rationale. long deployedEra = (m_view.IsEnsembleMember() && g_ensBestEra >= 0) ? g_ensBestEra : m_view.EraCount(); int markerFlags = m_view.ActiveFileCommon() ? FILE_COMMON : 0; string markerFile = m_view.ActiveFileName() + ".dbfill"; if(FileIsExist(markerFile, markerFlags)) { //--- FILE_SHARE_READ|FILE_SHARE_WRITE on every open, without exception - a sibling chart holding //--- this file open must not turn a skip-check into a hard failure. int mh = FileOpen(markerFile, markerFlags | FILE_TXT | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE); if(mh != INVALID_HANDLE) { long stampedEra = StringToInteger(FileReadString(mh)); FileClose(mh); if(stampedEra == deployedEra) { m_dbBackfillDone = true; PrintVerbose(m_view.Id() + ": pattern-database backfill already done for era " + IntegerToString((int)deployedEra) + " - skipping (its rows are still in the DB)."); return; } } } m_dbBackfillEra = deployedEra; dbm.OpenDatabase(); //--- Frozen batch-norm statistics, exactly like pass 3's OOS scoring walk - an unfrozen forward //--- pass would let the running stats drift while scoring. net.SetBatchNormFrozen(true); m_dbBackfillBars = bars; //--- Walks [calibLo, calibHi) from its OLDEST bar down to its newest - i.e. oldest -> newest in //--- TIME, which is the order ProcessSignal's outdated-row guard requires. m_dbBackfillStartIndex = (int)MathMin(calibHi - 1, bars - MathMax(m_view.HistoryBars(), 0) - 2); m_dbBackfillStopIndex = (int)MathMax(2, calibLo); m_dbBackfillIndex = m_dbBackfillStartIndex; m_dbBackfillFired = 0; m_dbBackfillActive = (m_dbBackfillStartIndex >= m_dbBackfillStopIndex); if(!m_dbBackfillActive) m_dbBackfillDone = true; // OOS window too short to walk - nothing to backfill, don't retry forever } //+------------------------------------------------------------------+ //| Time-boxed slice of the backfill walk - same chunking doctrine as | //| every other long walk in this file. Walks OLDEST -> NEWEST (mirrors| //| pass 3's own descent) because ProcessSignal()'s outdated-row guard | //| rejects a registration OLDER than a row its table already holds. | //+------------------------------------------------------------------+ void COnlineLearning::AdvancePatternDatabaseBackfill(void) { const uint DB_BACKFILL_TIME_BUDGET_MS = 80; uint chunkStartTick = GetTickCount(); dbm.BeginTransaction(); //--- ConfidenceTierNow() reads the live dPrevSignal field (the panel/RefreshLatestSignal's source of //--- truth) - borrowed per bar below to get the SAME tier bucketing a live vote would have used, //--- then restored so this backfill walk never leaks into the live-facing signal. double savedPrevSignal = m_view.PrevSignal(); CArrayDouble *td = m_view.TempData(); CNet *net = m_view.NetPtr(); for(; m_dbBackfillIndex >= m_dbBackfillStopIndex; m_dbBackfillIndex--) { if(GetTickCount() - chunkStartTick >= DB_BACKFILL_TIME_BUDGET_MS) break; int oi = m_dbBackfillIndex; if(!(oi < (int)(m_dbBackfillBars - MathMax(m_view.HistoryBars(), 0) - 1) && m_view.HasLabel(oi))) continue; if(!m_view.BuildFeatureWindow(oi) || !net.feedForward(td)) continue; net.getResults(td); double oSignal = (m_view.OutputNeurons() == 3) ? m_view.ApplyClassificationSoftmax() : td[0]; double oDeploySignal = (m_view.OutputNeurons() == 3) ? m_view.AdjustedSignalFromSoftmax() : oSignal; ENUM_SIGNAL dir = m_view.SignalFromValue(oDeploySignal); if(dir != Buy && dir != Sell) continue; // Neutral/abstained - live voting would not have buffered a row for this bar either m_view.SetPrevSignal(oDeploySignal); int tier = m_view.ConfidenceTierNow(); //--- Label agreement is the row's outcome, and the mark is the close of the bar the label //--- resolved on - the earliest bar the call could have been judged, not a fabricated barrier //--- touch. if(!m_view.HasLabel(oi)) continue; bool tradeWon = (dir == Buy) ? m_view.IsBuyLabel(oi) : m_view.IsSellLabel(oi); double atr = m_view.AtrAt(oi); double closeAt = m_view.CloseAt(oi); if(!MathIsValidNumber(atr) || atr <= 0.0 || !MathIsValidNumber(closeAt) || closeAt <= 0.0) continue; double spread = m_view.SpreadPrice(); int resolveIdx = oi - MathMax(m_view.LabelResolveAge(oi), 1); if(resolveIdx < 0) resolveIdx = 0; double entryPrice = (dir == Buy) ? closeAt + spread : closeAt; double exitPrice = m_view.CloseAt(resolveIdx); if(!MathIsValidNumber(exitPrice) || exitPrice <= 0.0) exitPrice = entryPrice; MqlDateTime t; TimeToStruct(m_view.BarTime(oi), t); string pattern = "Pattern_" + IntegerToString(tier); string dirStr = (dir == Buy) ? "Buy" : "Sell"; string tableName = m_view.PatternTableName(m_view.FilterId(), pattern, dirStr); double netVote = (dir == Buy) ? m_view.PatternWeightForTier(tier) : -m_view.PatternWeightForTier(tier); m_view.RegisterSignalRow(t.year, t.mon, t.day, t.day_of_week, t.hour, t.min, tableName, pattern, dirStr, entryPrice, exitPrice, tradeWon ? "Profit" : "Loss", netVote); m_dbBackfillFired++; } m_view.SetPrevSignal(savedPrevSignal); dbm.CommitTransaction(); if(m_dbBackfillIndex >= m_dbBackfillStopIndex) return; // more slices to come net.SetBatchNormFrozen(false); m_dbBackfillActive = false; m_dbBackfillDone = true; g_forcePatternWeightsRefresh = true; //--- Stamp the marker only now, on completion: a walk interrupted half way (EA removed mid-chunk) //--- leaves NO marker, so the next attach redoes it in full rather than ranking on a partial window. //--- The duplicate rows that costs are the lesser error - a half-filled table is silently biased //--- toward whichever end of the OOS window happened to finish. { int markerFlags = m_view.ActiveFileCommon() ? FILE_COMMON : 0; int mh = FileOpen(m_view.ActiveFileName() + ".dbfill", markerFlags | FILE_TXT | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE); if(mh != INVALID_HANDLE) { FileWriteString(mh, IntegerToString((int)m_dbBackfillEra)); FileClose(mh); } } Print(m_view.Id() + ": pattern database backfilled from " + IntegerToString(m_dbBackfillFired) + " calls on the" " held-out CALIBRATION band (bars " + IntegerToString(m_dbBackfillStopIndex) + ".." + IntegerToString(m_dbBackfillStartIndex) + ", era " + IntegerToString((int)m_dbBackfillEra) + ") - this IS the deploy-time warm-up: it runs with the weights FinalizeTrainRun just restored," " so the per-pattern win-rate history describes exactly what is about to trade and no separate" " backtest is needed first. Those bars were never trained on, never graded by pass 3 and never" " seen by the deploy gate. Two honest caveats: they are SIMULATED triple-barrier outcomes at" " today's spread rather than realised fills, and m_dirConfThreshold was fitted on this same" " band, so coverage here is mildly optimistic. Small tiers are shrunk toward the pooled rate" " before they become weights (see WinRateFromCounts)."); } //+------------------------------------------------------------------+ //| Online continual-learning step - LIVE CHART ONLY. Once a model | //| is deployed it keeps learning from real market structure the | //| same supervised way it was trained: predicting the swing label | //| of each bar, once that bar's pivot pair has committed. | //+------------------------------------------------------------------+ void COnlineLearning::OnlineLearnStep(void) { //--- Hard gates. InferenceOnly() covers BOTH the single backtest and every optimization pass: 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_view.InferenceOnly() || m_view.TrainRunActive()) return; if(!m_view.TrainingComplete() || m_view.TrainingStopRequested() || m_view.TrainingPaused()) return; if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD)) return; // belt-and-braces: never adapt weights inside any tester context CNet *net = m_view.NetPtr(); 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) int outputNeurons = m_view.OutputNeurons(); if(outputNeurons != 1 && outputNeurons != 3) return; //--- The frontier is the newest CLOSED bar; whether a bar is learnable is decided per bar by its //--- label's own finality (BarLabel returns Undefine until the pivot pair commits). int conf = 1; int barsAvail = m_view.AvailableBars(); //--- 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_view.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); //--- HOLD RATHER THAN LEARN ON A SHORT WINDOW. `need` is the minimum that reaches the //--- confirmation frontier WITH a full feature window behind it; below it the swing block //--- silently degrades and the features stop matching the ones the model was fitted on. int servable = m_view.ServableBars(wantBars, "online learning"); if(servable < need) return; wantBars = servable; if(!m_view.ResizeBuffers(wantBars) || !m_view.RefreshData()) return; //--- Same now-relative invalidation RefreshConvergedSignal() does, and needed independently of //--- it: this runs on a DEEPER bar grid (wantBars reaches the confirmation frontier, that one //--- only reaches the newest feature window), so the two legitimately disagree about `bars` and //--- each must re-key the cache for the grid it is about to read. m_view.EnsureBarCachesCapacity(wantBars); datetime frontierTime = m_view.BarTime(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. 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). double forecast = m_view.Forecast(); if(m_onlineRollingAcc < 0.0) m_onlineRollingAcc = (forecast > 0.0 && forecast <= 100.0) ? forecast : 100.0; //--- Guardrail floor: deploy baseline minus a margin, never below the absolute minimum. double baseline = (forecast > 0.0 && forecast <= 100.0) ? forecast : 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_view.BarTime(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. double savedEta = g_eta; g_eta = m_view.ModelEta() * ONLINE_LEARN_ETA_SCALE; CArrayDouble *td = m_view.TempData(); int learned = 0; for(int idx = oldestIdx - 1; idx >= conf; idx--) { datetime bt = m_view.BarTime(idx); if(bt <= m_onlineLearnedUpToTime) continue; // already learned (defensive; the walk above should exclude it) //--- UNRESOLVED: this bar's pivot pair has not committed, and (pivots being shared) neither //--- has any newer bar's. Stop WITHOUT advancing the watermark, so the walk resumes here once //--- the pair commits - that is the finality rule applied to online learning. if(m_view.BarLabel(idx) == Undefine) break; //--- Build this bar's feature window - IDENTICAL to Train()/RefreshLatestSignal(): ends AT bar idx //--- and extends the history window into the past. No lookahead (all bars are older than idx). if(!m_view.BuildFeatureWindow(idx)) { //--- 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(td); net.getResults(td); double predSignal = (outputNeurons == 3) ? m_view.ApplyClassificationSoftmax() : td[0]; //--- Same target rule training used. `idx` is at or beyond the confirmation frontier (conf == //--- the horizon, enforced above), so the forward window this reads is fully closed. ENUM_SIGNAL trueSignal = m_view.BarLabel(idx); bool hit = (m_view.SignalFromValue(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. double pBuy = (td.Total() > 0) ? td.At(0) : 0.0; double pSell = (td.Total() > 1) ? td.At(1) : 0.0; double pNeutral = (td.Total() > 2) ? td.At(2) : 0.0; //--- Build the target vector - identical encoding to Train()/AdvanceOosSimulationChunk(). bool buy = (trueSignal == Buy); bool sell = (trueSignal == Sell); td.Clear(); if(outputNeurons == 1) td.Add(buy && !sell ? 1 : (!buy && sell ? -1 : 0)); else { td.Add(buy ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); td.Add(sell ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); td.Add((!buy && !sell) ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW); } net.backProp(td, SampleWeight(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(m_view.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(m_view.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. g_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_view.FlattenIndicatorParams(ip); bool saveOk = net.Save(m_view.ActiveFileName() + ".nnw", m_view.ErrorPct(), m_view.UndefinePct(), m_view.Forecast(), m_view.StudiedTime(), m_view.ActiveFileCommon(), m_view.EraCount(), m_view.TrainingComplete(), ip); if(!saveOk) Print(m_view.Id() + ": ERROR - online-learning Net.Save failed for " + m_view.ActiveFileName() + ".nnw. Retrying next persist interval instead of resetting the bars-since-persist counter."); SaveShadowNet(ip); if(!m_view.SaveModelStatsNow()) Print(m_view.Id() + ": ERROR - online-learning SaveModelStats failed for " + m_view.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(m_view.Id() + ": online-learning checkpoint saved (" + IntegerToString((int)m_onlineSamples) + " total updates, rolling acc " + DoubleToString(m_onlineRollingAcc, 1) + "%)"); } } } //+------------------------------------------------------------------+ void COnlineLearning::SaveShadowNet(const double &indicatorParams[]) { if(CheckPointer(m_shadowNet) == POINTER_INVALID) return; m_shadowNet.Save(m_view.ActiveFileName() + "_shadow.nnw", m_view.ErrorPct(), m_view.UndefinePct(), m_view.Forecast(), m_view.StudiedTime(), m_view.ActiveFileCommon(), m_view.EraCount(), m_view.TrainingComplete(), indicatorParams); } //+------------------------------------------------------------------+ void COnlineLearning::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. CNet *net = m_view.NetPtr(); if(CheckPointer(net) != POINTER_INVALID && net.CpuInference()) return; string shadowFile = m_view.ActiveFileName() + "_shadow.nnw"; if(FileIsExist(shadowFile, m_view.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_view.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(m_view.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. 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_view.ActiveFileName() + "_shadowclone.tmp"; int cloneFlags = m_view.ActiveFileCommon() ? FILE_COMMON : 0; double ip[]; if(!net.Save(cloneFile, 0.0, 0.0, 0.0, m_view.StudiedTime(), m_view.ActiveFileCommon(), m_view.EraCount(), m_view.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_view.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. PrintVerbose(m_view.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(m_view.Id() + ": EMA shadow net bootstrapped from the main model's current weights (its own network instance - hence a second compute-backend init)"); } #endif // WARRIOR_ONLINELEARNING_ONLINELEARNING_MQH //+------------------------------------------------------------------+