refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
//+------------------------------------------------------------------+
//| 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 ( ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- 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 ) ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
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 ( ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
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 ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
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 |
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//| same supervised way it was trained: predicting the swing label |
//| of each bar, once that bar's pivot pair has committed. |
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
//+------------------------------------------------------------------+
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 ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- 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 ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
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)
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- 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 ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
//--- 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
//+------------------------------------------------------------------+