forked from animatedread/Warrior_EA
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
769 lines
42 KiB
MQL5
769 lines
42 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|
|
//+------------------------------------------------------------------+
|