Warrior_EA/Expert/AIBase/Lifecycle.mqh
AnimateDread 788115970c fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".

1. THE DRAW. The filtered overlay armed on the FIRST member to finish
   pass 3 and leaned on a 60 s rate limit to "collapse the burst",
   assuming members finish seconds apart. They do not - on USDJPY one
   member was at sample 10496 of pass 2 while another was at 2304,
   minutes apart. A member with no era-end snapshot returns false from
   SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
   BEFORE `den += ModuleWeight()`, so the one finished model's tier
   weight became the entire vote and was drawn as a consensus arrow.

   An abstention is a member that looked at the bar and said nothing; a
   missing snapshot is a member that has not looked. The first must
   dilute the vote, the second must suppress the draw. The arm is now a
   readiness MASK - one bit per m_ensembleIndex, set at that member's
   pass-3 completion, cleared when a sweep arms - and a sweep waits for
   every enrolled member. Bounded at 10 minutes so a member that stops
   cannot freeze the chart, and the partial draw PRINTS which members
   were missing: the be39674 lesson is that a hold must never silence
   the thing that reports it.

2. THE VOTE ITSELF, which is the worse half and is not display-only.
   Tier weights are not persisted in the .nnw - they exist only as the
   output of a completed pass 3 - so before a member's first
   RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
   Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
   call enters the capability-weighted mean claiming a 100% win rate
   beside ranked members contributing ~25. Not a strong opinion: the
   wrong unit. One unranked member drags the ensemble over any
   threshold, on every fresh deploy and every resume. USDJPY has a
   measured ceiling of ~19 and was firing anyway.

   LiveVoteContribution() now abstains until self-ranked, which drops
   the member from the sum AND the divisor. One function, so live and
   the gate move together (2c443ba).

Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00

1096 lines
48 KiB
MQL5

//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Lifecycle.mqh |
//| |
//| Construction/destruction, the CExpertSignal vote API |
//| (LongCondition/ShortCondition/ConfidenceTier/pattern weights), |
//| tick + chart-event dispatch, and the per-config chart lock. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_AIBASE_LIFECYCLE_MQH
#define WARRIOR_AIBASE_LIFECYCLE_MQH
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
//--- These are only fallback defaults for a fresh object before the EA's OnInit() applies the
//--- active input values via the public setters in Warrior_EA.mq5. The input-driven values are the
//--- source of truth for the actual run configuration.
CExpertSignalAIBase::CExpertSignalAIBase(void) :
ID("NULL"),
m_neuronsCount(0),
m_minTrainYear(1970),
m_optimizationAlgo(TrainingOptimizer), // see the member declaration comment
//--- Placeholder only; InitNeuralNetwork() replaces it with ComputeFirstLayerWidth() before anything
//--- reads it. Deliberately the floor rather than 0, so a hypothetical path that built a topology
//--- without going through init would produce a small usable net instead of a zero-width layer.
m_initialNeuronsCount(FIRST_LAYER_MIN_WIDTH),
m_outputNeuronsCount(OUTPUT_CLASSIFICATION),
//--- Frozen. Nothing reads these to build a topology any more - the taper derives its own
//--- endpoints (BuildFreshTopology) - but they still occupy positional slots in the .cfg sidecar
//--- and the weights fingerprint.
m_minNeuronsCount(MIN_NEURONS_20),
m_neuronsReduction(RF_70),
m_hiddenLayersCount(3),
m_lstmHiddenSize(32),
m_convFilterCount(16),
m_historyBars(14),
m_fractalPeriods(5),
m_pattern_0(25),
m_pattern_1(50),
m_pattern_2(75),
m_pattern_3(100),
m_trainTarget(0),
m_ensembleMember(false),
m_ensemblePanelSlot(-1),
m_fracLegCount(0),
m_metaCandCount(0),
m_useVolumes(true),
m_useTime(true),
m_useATR(true),
m_useMA(false),
m_useRSI(false),
m_useMACD(false),
m_useIchimoku(false),
m_useSwingContext(false),
m_useNews(false),
m_useCrossAsset(false),
m_useSpreadFeature(false),
m_spreadSeriesBars(0),
m_spreadSeriesAnchor(0),
m_crossAssetAnchor(0),
m_crossAssetPairsPinned(""),
m_crossAssetCfgSaved(false),
m_useAltData(false),
m_altDataEnabled(true),
m_altDataLateWarned(false),
m_altDataNamesPinned(""),
m_newsFeatureWindowMinutes(60),
m_useADCumulativeDelta(false),
m_useADShorteningOfThrust(false),
m_useADWyckoffEventStream(false),
m_useADWyckoffFailedStructure(false),
m_useADWyckoffSignificantBarInversion(false),
m_autoTuneIndicators(false),
m_indicatorsPtr(NULL),
Net(NULL),
m_shadowNet(NULL),
TempData(NULL),
dError(-1),
dUndefine(0),
dForecast(0),
dPrevSignal(0),
m_refreshOk(0),
m_refreshFailFeatures(0),
m_refreshFailShort(0),
m_refreshBuy(0),
m_refreshSell(0),
m_refreshNeutral(0),
m_voteGateBlocked(0),
m_voteGatePassed(0),
m_voteGateCompleteAtFirst(-1),
m_voteGateLoadedAtFirst(-1),
m_signalClusterWindow(6),
m_nmsLiveBuyTime(0),
m_nmsLiveSellTime(0),
m_nmsLiveBuyAccept(false),
m_nmsLiveSellAccept(false),
m_nmsLiveKeptTime(0),
m_nmsLiveKeptDir(Neutral),
m_nmsLiveKeptConf(0),
dtStudied(0),
m_eraCount(0),
m_trainingComplete(false),
m_inferenceOnly(false),
m_modelLoadedFromDisk(false),
m_topologySuperseded(false),
m_mqlInferenceValidated(false),
m_shadowBootstrapAttempted(false),
m_enableOnlineLearning(true),
m_freezePriorCalibration(false),
m_onlineLearnedUpToTime(0),
m_onlineRollingAcc(-1.0),
m_onlineSamples(0),
m_onlineBarsSincePersist(0),
m_onlineBlendFrozen(false),
bEventStudy(false),
m_oosSplitPct(30),
dOosError(-1),
dOosForecast(0),
m_oosSamples(0),
m_cumIsCorrect(0),
m_cumIsTotal(0),
m_cumOosCorrect(0),
m_cumOosTotal(0),
m_oosOutSpreadSum(0),
m_oosOutCount(0),
m_spreadAtr(0.0),
m_oosNeutralStrict(0),
m_oosNeutralTie(0),
m_oosTieBuySell(0),
m_oosRailBars(0),
m_countBuySignals(0),
m_countSellSignals(0),
m_countNeutralSignals(0),
m_trueBuyCount(0),
m_trueSellCount(0),
m_trueNeutralCount(0),
m_logitAdjustTau(1.0),
m_logitAdjustLogged(false),
m_logitAdjustSkipWarned(false),
m_prevEraTrueBuyCount(0),
m_prevEraTrueSellCount(0),
m_prevEraTrueNeutralCount(0),
m_oosBuyHits(0),
m_oosBuyTotal(0),
m_oosSellHits(0),
m_oosSellTotal(0),
m_oosNeutralHits(0),
m_oosNeutralTotal(0),
m_oosBuyPredicted(0),
m_oosBuyPredictedHits(0),
m_oosSellPredicted(0),
m_oosSellPredictedHits(0),
m_oosNeutralPredicted(0),
m_oosNeutralPredictedHits(0),
m_oosBuyPredictedWins(0),
m_oosSellPredictedWins(0),
m_oosWinLongTotal(0),
m_oosWinShortTotal(0),
m_oosConfidenceSum(0),
m_confidenceCalScale(1.0),
m_minDirectionalRecallPct(40),
m_lastProgressLogTick(0),
//--- No vote-driven exit until the inputs say otherwise - matches Signal_ThresholdClose's shipped Disabled.
m_exitVoteThreshold(0.0),
m_exitHoldToBarrier(false),
m_simRSum(0.0),
m_simRSumSq(0.0),
m_simTrades(0),
m_simVoteExits(0),
m_simBarrierWins(0),
m_simTpHits(0),
m_closeAllCycleBars(0),
m_closeAllMeanBudget(0),
m_geoDiffSum(0.0),
m_geoDiffSumSq(0.0),
m_geoIncSum(0.0),
m_geoCandSum(0.0),
m_geoTrades(0),
m_geoIncOpen(0),
m_geoCandOpen(0),
m_geoCandSl(0.0),
m_geoCandTp(0.0),
m_geoStartTick(0),
m_simTimeouts(0),
m_simTimeoutRSum(0.0),
m_lastTimeoutShare(-1.0),
m_lastTimeoutMeanR(0.0),
m_exitReplayReported(false),
m_lastRecallFloorPct(0.0),
m_detectabilityReported(false),
m_priorBuy(0.0),
m_priorSell(0.0),
m_priorNeutral(0.0),
m_oosBuyFired(0),
m_oosBuyFiredHits(0),
m_oosSellFired(0),
m_oosSellFiredHits(0),
m_oosNmsFired(0),
m_oosNmsHits(0),
m_oosNmsLastBuyIdx(-1),
m_oosNmsLastSellIdx(-1),
m_oosNmsKeptIdx(-1),
m_oosNmsKeptConf(0.0),
m_oosNmsKeptDir(Neutral),
m_lastBuyFiredPrecPct(-1),
m_lastSellFiredPrecPct(-1),
m_lastBuyFired(0),
m_lastSellFired(0),
m_swingConfirmationBars(100),
m_barrierHorizonBars(BARRIER_HORIZON_FALLBACK),
m_barrierHorizonResolved(false),
m_barrierFallbackWarned(false),
m_lastBarrierTimedOut(false),
m_lastLabelWeekendCut(false),
m_swingMedianLegAtr(0.0),
m_metaGateArmed(false),
m_metaGateLastP(-1.0),
m_metaGateLastBe(-1.0),
m_metaGateApproved(0),
m_metaGateVetoed(0),
m_barrierHorizonLegStarved(false),
m_horizonStarvedWarned(false),
m_geometryCfgSaved(false),
m_geometryAdopted(false),
m_dirEvidence(false),
m_dirEvidenceWhy("not measured yet"),
m_lastBarrierBothWon(false),
m_lastBarrierBothWonTied(false),
m_labelPrebuildTimeoutCount(0),
m_labelPrebuildWeekendCutCount(0),
m_labelPrebuildBothWonCount(0),
m_labelPrebuildBothWonTieCount(0),
m_maxErasPerRun(300),
m_arrowRestoreIndex(0),
m_arrowRestorePending(false),
m_arrowRestoreStartMs(0),
m_rescanIndex(0),
m_rescanHi(0),
m_rescanBarsNow(0),
m_rescanPending(false),
m_rescanStartMs(0),
m_rescanRawBuy(0),
m_rescanRawSell(0),
m_rescanRawNeutral(0),
m_trainRunActive(false),
m_eraResumePending(false),
m_resumeBars(0),
m_resumeTotalIter(0),
m_resumeOosCutoff(0),
m_resumeBarIndex(0),
m_resumeAddLoop(false),
m_isTrainQueueCount(0),
m_isTrainCursor(0),
m_isPass2Active(false),
m_isPass2Done(false),
m_isPass3Active(false),
m_oosScoreIndex(0),
m_oosScoreStartIndex(0),
m_isCalibActive(false),
m_isCalibDone(false),
m_calibIndex(0),
m_calibStartIndex(0),
m_excNet(NULL),
m_excTgt(NULL),
m_excOut(NULL),
m_excHeadFailed(false),
m_excBaseTotal(0),
m_excScored(0),
m_excScoredD(0),
m_excDiffSum(0.0),
m_excDiffSumSq(0.0),
m_excTrailDiffSum(0.0),
m_excTrailDiffSumSq(0.0),
m_excMonoViol(0),
m_excTrainTick(0),
m_excUs(0),
m_excTrailHead(0),
m_excTrailCount(0),
m_excTrailN(0),
m_excTrailScored(0),
m_lastStatusLabelUpdateTick(0),
m_lastBuyRecallPct(-1),
m_lastSellRecallPct(-1),
m_lastDisplayNeuron0(0),
m_lastDisplayNeuron1(0),
m_lastDisplayNeuron2(0),
m_lastDisplaySignal(0),
m_lastBarTime(0),
m_modelEta(InitialEtaForOptimizer()),
m_etaCeiling(InitialEtaForOptimizer()),
m_erasSinceCooldown(0),
m_bestOosForecast(-1),
m_bestBalancedOos(-1),
m_bestPassedRecall(false),
m_bestBothSidesLive(false),
m_eraStartTick(0),
m_passFeatUs(0),
m_passNetUs(0),
m_passHeartbeatPrints(0),
m_passWindowOk(0),
m_passWindowFail(0),
m_consecutiveRegressions(0),
m_featureFailTransient(false),
m_featureFailBlock(""),
m_featureFailIdx(-1),
m_windowFailSlot(-2),
m_windowFailTotal(0),
m_featureWidthWarned(false),
m_featureHealthReported(false),
m_lastHeartbeatTick(0),
m_passProgressPct(0),
m_passLabel("starting"),
m_lastEraCompleteTick(0),
m_lastStallReportTick(0),
m_haveOosCheckpoint(false),
m_bestDirPrecPct(-1.0),
m_bestChancePrecPct(-1.0),
m_bestDirCalls(0),
m_deployCandidateEras(0),
m_oosStable(false),
m_objectiveMet(false),
m_erasSinceBestBalanced(0),
m_plateauStage(0),
m_bestIsError(-1.0),
m_erasSinceBestIsError(0),
m_isErrorPlateaued(false),
m_restartBoostErasLeft(0),
m_syncWaitStartTick(0),
m_warmupPassesRemaining(0),
m_coldSweepTick(0),
m_indicatorDepthCapBars(0),
m_indicatorDepthDeadWarned(false),
m_handleRepairTick(0),
m_barrierEraSeen(-1),
m_barrierEraTick(0),
m_barrierExcluded(false),
m_barrierPhaseProgress(false),
m_barrierHoldReportTick(0),
m_inferenceDepthRefusalWarned(false),
m_prebuildBlockWarned(false),
m_depthSettleStart(0),
m_depthProbeTick(0),
m_depthProbeLast(0),
m_depthProbeStable(0),
m_labelCacheBars(0),
m_labelCacheAnchorTime(0),
m_labelCachePrebuilt(false),
m_lastExcUp(0.0),
m_lastExcDown(0.0),
//--- 0 samples => MeanLabelLifespan() returns 1.0 => EffectiveSampleSize() is the identity, so an
//--- un-prebuilt model behaves exactly as it did before the overlap correction existed rather than
//--- shrinking its own samples on a guess. See m_lastLabelLifespan.
m_lastLabelLifespan(0),
m_labelLifespanSum(0.0),
m_labelLifespanCount(0),
//--- -1 = the deploy gate has not run its arithmetic yet this era; the era line then omits the bar
//--- rather than printing a stale one from a previous era.
m_lastEdgeFloorPct(-1.0),
m_lastPrecSE(-1.0),
m_lastEffN(-1.0),
m_lastRungLifespan(0.0),
m_poolWriteWarned(false),
m_lastPoolPasses(false),
m_lastPoolReport(""),
m_derivedSlMult(0.0),
m_derivedTpMult(0.0),
//--- 0.0 = unthresholded until the first pass 2 fits it (see DIR_CONF_THRESHOLD_BINS). Deliberately
//--- the permissive value: a model that has not measured its own operating point must not silently
//--- abstain on everything.
m_dirConfThreshold(0.0),
m_bestDirConfThreshold(0.0),
m_dirConfPrimaryBars(0),
m_dirConfSparseWarned(false),
m_geometryDerived(false),
m_geometryDerivePasses(0),
m_swingMedianBars(0),
m_labelPrebuildActive(false),
m_prebuildSeedPending(false),
m_labelPrebuildBars(0),
m_labelPrebuildOosCutoff(0),
m_labelPrebuildIndex(-1),
m_labelPrebuildBuyCount(0),
m_labelPrebuildSellCount(0),
m_labelPrebuildNeutralCount(0),
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),
m_tuneTrialIndex(-1),
m_tuneBestOosForecast(-1),
m_tuneLastTrialWasWin(true),
m_tuneHaveBestCheckpoint(false),
m_tuneStartTrainBar(0),
m_tuneFilterDone(false),
m_trainingPaused(false),
m_trainingStopRequested(false),
m_activeFileCommon(true),
m_configLockName(""),
m_isInitialized(false),
m_shutdownInProgress(false),
m_lastArrowsSaved(0),
m_purgeMismatchWarned(false),
m_miBestColumn(0.0),
m_miLabelEntropy(0.0),
m_miStrideBars(0),
m_miNullBlocks(0),
m_miReportDone(false),
m_tiersSelfRanked(false),
m_baselineDone(false),
m_overlaySnapBars(0),
m_prospectiveSigSnap(-2.0),
m_dispSignal(0.0),
m_dispValid(false),
m_dispStamp(0),
m_dispEra(-1),
m_lastEnsRefusalKey(0),
m_miReportDeferrals(0),
m_barrierScanSlMult(0.0),
m_barrierScanTpMult(0.0),
m_barrierScanLiveLabels(false),
m_barrierScanTimeouts(0),
m_barrierHorizonClamped(false)
{
//--- Claim this instance's study-event id - see STUDY_EVENT_ID_BASE (ExpertSignalAIBase.mqh) for why
//--- these are per instance and offset above the Controls library's event codes.
m_studyEventId = (ushort)(STUDY_EVENT_ID_BASE + g_warriorStudyEventSeq++);
m_studyArmedTick = 0;
m_ensembleIndex = -1; // not an ensemble member until EnsembleMember(true) registers one (the flag itself is in the init list)
//--- per-era stash the ensemble verdict reads (see EnsembleStashEraStats); -1/false = "no era yet"
m_eraStatPrecPct = -1.0;
m_eraStatChancePct = -1.0;
m_eraStatCalls = 0;
m_eraStatTradeable = false;
m_eraStatTwoSided = false;
m_eraStatScore = 0.0;
m_eraStatBlended = 0.0;
m_eraStatThreshold = 0.0;
m_checkpointEra = -1;
//--- indicator tuning defaults live in CADIndicatorTuner's own constructor (Expert\ADIndicatorTuner.mqh),
//--- which runs automatically for the m_indicatorTuner member above.
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalAIBase::~CExpertSignalAIBase(void)
{
//--- deliberately NOT calling PersistOnShutdown() here: OnDeinit() (Warrior_EA.mq5) already
//--- calls it explicitly for every signal, one call stack frame shallower, BEFORE
//--- Expert.Deinit() tears these objects down.
if(CheckPointer(Net) != POINTER_INVALID)
delete Net;
if(CheckPointer(m_shadowNet) != POINTER_INVALID)
delete m_shadowNet;
if(CheckPointer(TempData) != POINTER_INVALID)
delete TempData;
if(CheckPointer(m_simOosNet) != POINTER_INVALID)
delete m_simOosNet;
//--- Excursion head: never persisted (Stage 1 is a measurement), so teardown is the whole lifecycle.
if(CheckPointer(m_excNet) != POINTER_INVALID)
delete m_excNet;
if(CheckPointer(m_excTgt) != POINTER_INVALID)
delete m_excTgt;
if(CheckPointer(m_excOut) != POINTER_INVALID)
delete m_excOut;
//--- Unconditional now that the warm-reload "leave the arrows up" branch is gone (see
//--- ShutdownChartCleanup). Cheap and idempotent: OnDeinit already purged, so this normally deletes
//--- nothing - it exists for the teardown paths that never reach OnDeinit (a failed OnInit).
PurgeChart();
//--- Last, and cheap by design (one global-variable delete): the claim must outlive every save above
//--- it, or a chart re-attaching during this teardown could start writing the same files mid-save.
ReleaseConfigLock();
}
//+------------------------------------------------------------------+
//| Sets the file/id identity a subclass constructor would otherwise |
//| repeat verbatim (ID, m_id, m_folderPath, m_fileName, pattern count)|
//+------------------------------------------------------------------+
void CExpertSignalAIBase::SetIdentity(string id, string shortId, int patternCount = 4)
{
ID = id;
m_id = shortId;
m_folderPath = eaName + "\\" + "Neural Networks" + "\\" + "State" + "\\" + m_id + "\\";
m_fileName = m_folderPath + _Symbol + "_" + IntegerToString(_Period);
m_pattern_count = patternCount;
}
//+------------------------------------------------------------------+
//| CONTROL-PANEL COMMAND. One switch, so a button can never reach |
//| some models and miss others: the panel resolves the direction |
//| once and every model in the tree is told the same thing. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase::OnSignalCommand(const ENUM_SIGNAL_COMMAND cmd)
{
switch(cmd)
{
case SIGCMD_PAUSE_TRAINING:
PauseTraining();
return true;
case SIGCMD_RESUME_TRAINING:
ResumeTraining();
return true;
case SIGCMD_STOP_TRAINING:
StopTraining();
return true;
case SIGCMD_START_TRAINING:
StartTraining();
return true;
//--- These three report SUCCESS rather than "I understood the command", because the panel
//--- counts them back to the operator and a failed deploy or a failed rebuild is exactly what
//--- they need told about.
case SIGCMD_DEPLOY:
return DeployNow();
case SIGCMD_RESET_WEIGHTS:
return ResetWeights();
case SIGCMD_SAVE_WEIGHTS:
return SaveWeightsNow();
case SIGCMD_LOAD_WEIGHTS:
return LoadWeightsNow();
case SIGCMD_RETRAIN_DEPLOYED:
RetrainDeployed();
return true;
//--- Queues a rescan; returns whether one was actually queued, which is what tells the EA to
//--- defer the "arrows shown" alert until every queued scan has drained.
case SIGCMD_RESCAN_SIGNALS:
return StartChartSignalRescan();
case SIGCMD_REPORT_IDENTITY:
Print(" " + RegistryLine());
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Panel button labels ask these; see ENUM_SIGNAL_TRAIT. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase::HasSignalTrait(const ENUM_SIGNAL_TRAIT trait)
{
switch(trait)
{
case SIGTRAIT_TRAINABLE:
return true;
case SIGTRAIT_TRAINING_PAUSED:
return m_trainingPaused;
case SIGTRAIT_TRAINING_STOPPED:
return m_trainingStopRequested;
case SIGTRAIT_TRAINING_COMPLETE:
return m_trainingComplete;
//--- "still trainable AND has never checkpointed an era that cleared the per-class recall
//--- floor" - the same bar the plateau ladder refuses to cross on its own.
case SIGTRAIT_DEPLOY_SKIPS_RECALL:
return (!m_trainingComplete && !m_bestPassedRecall);
case SIGTRAIT_RESCAN_PENDING:
return m_rescanPending;
}
return false;
}
//+------------------------------------------------------------------+
//| "Voting" that price will grow. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase::LongCondition(void)
{
int result = 0;
//--- Readiness gate: live trading still requires a converged model, but an inference-only tester
//--- run may replay a model that was ACTUALLY loaded from disk even if its persisted
//--- trainingComplete flag is false.
NoteVoteGate(DoubleToSignal(dPrevSignal) == Buy);
if(!m_trainingComplete && !(m_inferenceOnly && m_modelLoadedFromDisk))
return 0;
//--- No alternation gate any more - see the removal note at m_voteGateBlocked's declaration.
//--- Under triple-barrier labels consecutive same-direction setups are ordinary and correct.
if(dPrevSignal == -2)
return 0;
//--- NO confidence floor here, by design - see m_minSignalConfidence's former declaration site. A
//--- weak call is not blocked at the AI's own boundary; it votes at its tier weight (as low as
//--- m_pattern_0) and is then filtered by Min vote to open, exactly like a weak classic vote.
if(DoubleToSignal(dPrevSignal) == Buy)
{
int tier = ConfidenceTier();
result = PatternWeightForTier(tier);
m_active_pattern = "Pattern_" + IntegerToString(tier);
m_active_direction = "Buy";
}
return(result);
}
//+------------------------------------------------------------------+
//| "Voting" that price will fall. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase::ShortCondition(void)
{
int result = 0;
//--- Readiness gate - see LongCondition's matching comment.
NoteVoteGate(DoubleToSignal(dPrevSignal) == Sell);
if(!m_trainingComplete && !(m_inferenceOnly && m_modelLoadedFromDisk))
return 0;
//--- "not yet studied" sentinel, and no confidence floor - see LongCondition's matching comments.
if(dPrevSignal == -2)
return 0;
if(DoubleToSignal(dPrevSignal) == Sell)
{
int tier = ConfidenceTier();
result = PatternWeightForTier(tier);
m_active_pattern = "Pattern_" + IntegerToString(tier);
m_active_direction = "Sell";
}
return result;
}
//+------------------------------------------------------------------+
//| Buckets a confidence magnitude into one of 4 equal bands between |
//| the head's own structural floor and 1.0 - see m_pattern_0's |
//| declaration comment for the resulting tier/weight table. Neither |
//| head has a configurable floor: the boundary is 1/3 for the 3-class|
//| softmax and 0.5 for regression (DoubleToSignal's own threshold), |
//| both arithmetic properties of the head rather than settings. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase::ConfidenceTierFor(const double signal)
{
//--- The head's own STRUCTURAL decision boundary - the lowest confidence magnitude that head can
//--- possibly report for a directional call - not a user setting: - 3-class classification: the
//--- winning class of a 3-way softmax is arithmetically >= 1/3, since three probabilities
//--- summing to 1 cannot all be below it.
double floorConf = (m_outputNeuronsCount == 3) ? (1.0 / 3.0) : 0.5;
double span = MathMax(1.0 - floorConf, 0.0001);
//--- RAW magnitude, not the calibrated one (fixed 2026-08-16).
double rawMag = MathAbs(signal);
if(!MathIsValidNumber(rawMag))
return 0;
double t = (MathMin(1.0, rawMag) - floorConf) / span;
int tier = (int)MathFloor(t * 4.0);
return MathMax(0, MathMin(tier, 3));
}
//+------------------------------------------------------------------+
//| The live bar's tier - the only caller shape that existed before |
//| ConfidenceTierFor() was split out, kept so LongCondition()/ |
//| ShortCondition() read exactly as they did. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase::ConfidenceTier(void)
{
return ConfidenceTierFor(dPrevSignal);
}
//+------------------------------------------------------------------+
//| The signed vote this member would cast for a given decision, in |
//| the units CExpertSignalCustom::Direction() sums - see the |
//| declaration comment for why this is a shared function and not |
//| two parallel expressions. |
//+------------------------------------------------------------------+
double CExpertSignalAIBase::LiveVoteContribution(const double signal)
{
//--- A MEMBER THAT HAS NEVER RANKED ITSELF DOES NOT VOTE. Until RankTiersFromOos() runs once,
//--- m_pattern_0..3 hold the constructor's stock 25/50/75/100 ladder - and since 4858507 the vote
//--- currency is a WIN RATE, so an unranked tier-3 call enters the mean claiming a 100% win rate
//--- against ranked members contributing ~25. That is not a strong opinion, it is the wrong unit:
//--- one unranked member drags the whole ensemble over any threshold. It bites on every fresh
//--- deploy AND every resume, because the tier weights are not persisted in the .nnw - they exist
//--- only as the output of a completed pass 3. Found 2026-08-23 on USDJPY, whose measured ceiling
//--- is ~19 and which was firing anyway.
if(!m_tiersSelfRanked)
return 0.0;
ENUM_SIGNAL s = DoubleToSignal(signal);
if(s != Buy && s != Sell)
return 0.0; // abstention - live drops it from the sum AND the divisor
double w = (double)PatternWeightForTier(ConfidenceTierFor(signal));
if(!MathIsValidNumber(w) || w <= 0.0)
return 0.0; // a pattern ranked to weight 0 contributes nothing and is not a voter
double contribution = m_weight * w;
if(!MathIsValidNumber(contribution))
return 0.0;
return (s == Buy) ? contribution : -contribution;
}
//+------------------------------------------------------------------+
//| Returns the given tier's current pattern weight (0-100) |
//+------------------------------------------------------------------+
int CExpertSignalAIBase::PatternWeightForTier(int tier)
{
switch(tier)
{
case 0:
return m_pattern_0;
case 1:
return m_pattern_1;
case 2:
return m_pattern_2;
default:
return m_pattern_3;
}
}
//+------------------------------------------------------------------+
//| TURN THIS ERA'S HELD-OUT OUTCOMES INTO THE VOTE WEIGHTS. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::RankTiersFromOos(void)
{
//--- SNAPSHOT FIRST, unconditionally - this runs at pass-3 completion, the single moment the
//--- arrow cache is complete for the era, and everything display-side (the overlay sweep, the
//--- prospective readout) reads the snapshot instead of the cache precisely because the cache is
//--- about to be wiped when the next era starts.
int snapN = MathMin(ArraySize(m_arrowSignalCache), SIGNAL_RESCAN_LOOKBACK_BARS + 16);
if(snapN > 0)
{
ArrayResize(m_overlaySigSnap, snapN);
ArrayCopy(m_overlaySigSnap, m_arrowSignalCache, 0, 0, snapN);
}
m_overlaySnapBars = snapN;
m_prospectiveSigSnap = -2.0;
for(int pi = 1; pi <= 16 && pi < snapN; pi++)
if(m_overlaySigSnap[pi] != -2.0 && MathIsValidNumber(m_overlaySigSnap[pi]))
{
m_prospectiveSigSnap = m_overlaySigSnap[pi];
break;
}
//--- ...and tell the EA THIS member's snapshot is fresh. The sweep waits for every enrolled
//--- member's bit - see g_warriorOverlayReadyMask for why a rate limit was not enough.
if(m_ensembleIndex >= 0 && m_ensembleIndex < ENS_MAX_MEMBERS)
g_warriorOverlayReadyMask |= (((uint)1) << m_ensembleIndex);
g_warriorOverlayArmRequest = true;
int pooledFired = 0, pooledHits = 0;
for(int t = 0; t < 4; t++)
{
pooledFired += m_oosTierFired[t];
pooledHits += m_oosTierHits[t];
}
//--- Nothing fired this era (all-Neutral, or a stopped/cap-hit era): leave the previous era's
//--- weights standing rather than collapsing every tier to a prior built on no evidence at all.
if(pooledFired <= 0)
return;
double pooledPct = 100.0 * pooledHits / pooledFired;
//--- WHAT A MEMBER WITH NO EVIDENCE IS WORTH: the coin-flip rate on this era's OOS bars, which is
//--- what the gate calls zero skill. Shrinking toward it means "few fires -> speaks at chance",
//--- where shrinking toward the member's own pooled rate would mean "few fires -> speaks at
//--- whatever those few fires said", which is no shrinkage at all.
int zsBars = m_oosBuyTotal + m_oosSellTotal + m_oosNeutralTotal;
double chancePct = (zsBars > 0)
? 50.0 * ((double)m_oosWinLongTotal + (double)m_oosWinShortTotal) / zsBars
: pooledPct;
double pooledEffN = MathMax(0.0, EffectiveSampleSize((double)pooledFired));
double trustPct = ShrunkRatePct(pooledEffN * ((double)pooledHits / pooledFired), pooledEffN,
chancePct, MODULE_PRIOR_EFF_N);
//--- WHY NOT WinRateFromCounts(), which is the estimator the classic ladders use: it returns
//--- NO_DATA_WIN_RATE for anything under MIN_TRADES_FOR_WIN_RATE raw trades, BEFORE it shrinks.
string line = "";
for(int t = 0; t < 4; t++)
{
int fired = m_oosTierFired[t];
int hits = m_oosTierHits[t];
double w = trustPct;
if(fired > 0)
{
double effN = MathMax(0.0, EffectiveSampleSize((double)fired));
double effHits = effN * ((double)hits / fired);
//--- Same estimator the classic ladders use (ShrunkRatePct), with the prior deliberately
//--- far smaller: it is counted in the same EFFECTIVE units as the evidence, and a tier
//--- holds ~8-15 of those, so the classic path's 100 would drown every tier in the pool.
//--- Toward the SHRUNK pooled rate, not the raw one: a member whose pooled evidence is
//--- thin must not hand its tiers a confident prior it does not have itself.
w = ShrunkRatePct(effHits, effN, trustPct, TIER_PRIOR_EFF_N);
}
//--- Rounded to the nearest INTEGER, not to the nearest 10 as NormalizeWinRate() does. After
//--- shrinkage the tiers legitimately sit within a few points of each other, and decade rounding
//--- would collapse them back into one number - undoing the separation this exists to produce.
int wi = (int)MathMax(0, MathMin(100, MathRound(w)));
ApplyTierWeight(t, wi);
line += StringFormat(" T%d=%d(%d fires, %.1f eff)", t, wi, fired,
(fired > 0 ? EffectiveSampleSize((double)fired) : 0.0));
}
//--- MODULE WEIGHT = how much this model's opinion COUNTS in the weighted mean, which since the
//--- 2026-08-18 currency change is a trust weight and no longer a discount on the estimate. The
//--- pooled holdout win rate is the honest measure of that trust.
Weight(MathMax(0.0, MathMin(1.0, trustPct / 100.0)));
m_tiersSelfRanked = true;
//--- THROTTLED (2026-08-19): the re-rank happens (and must happen) every era, but saying so
//--- every era was ~950 near-identical lines/member/day once the system was confirmed working.
//--- The weights it prints are visible live on the member HUD lines anyway.
if(TrainLogDue())
Print(ID + StringFormat(": tier weights re-ranked from %d held-out fires (%.1f effective,"
" pooled %.1f%% raw -> %.1f%% shrunk toward the %.1f%% coin-flip rate on"
" %.0f prior-equivalent calls) ->%s | module weight %.2f. These are the"
" weights the NEXT era votes with.",
pooledFired, pooledEffN, pooledPct, trustPct, chancePct,
MODULE_PRIOR_EFF_N, line, ModuleWeight()));
}
//+------------------------------------------------------------------+
//| Set the specified pattern's weight to the specified value |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::ApplyPatternWeight(int patternNumber, int weight)
{
//--- THE SIGNAL DB DOES NOT OUTRANK THE HOLDOUT. UpdateSignalsWeights() calls this hourly for
//--- every filter it can find rows for, and for an AI filter those rows are LIVE-journaled fires
//--- accumulated across eras - i.e. across models. Without this the ranking pass would silently
//--- undo every era's self-ranking within the hour.
if(m_tiersSelfRanked)
return;
switch(patternNumber)
{
case 0:
Pattern_0(weight);
break;
case 1:
Pattern_1(weight);
break;
case 2:
Pattern_2(weight);
break;
case 3:
Pattern_3(weight);
break;
default:
break;
}
}
//+------------------------------------------------------------------+
//| OnTick function |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::OnTickHandler(void)
{
ScheduleTrainingIfNeeded();
}
//+------------------------------------------------------------------+
//| Schedules the next training pass (if one is due) and refreshes |
//| the per-tick status label. Factored out of OnTickHandler() so |
//| Warrior_EA.mq5's always-on timer (see PollTraining()) can drive |
//| this on a fixed wall-clock schedule too - training must not stall |
//| just because the market is closed and no ticks are arriving. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::ScheduleTrainingIfNeeded(void)
{
//--- stopped: no new training passes get scheduled at all (StartTraining() re-arms this).
//--- paused: still schedule so bEventStudy/dtStudied bookkeeping stays current, but Train() itself
//--- blocks at the next era boundary until resumed - keeps in-memory state coherent either way.
//--- complete: training already converged - a plain new bar must NOT re-enter Train()'s full era
//--- loop, which would otherwise reset the best-checkpoint/g_eta-decay tracking and run real
//--- Net.backProp() passes again, forever, once per bar, on an already-converged model (see
//--- RefreshConvergedSignal()'s declaration comment). Just keep the live signal current instead.
//--- publish this signal's current signed confidence for the intelligent trailing (and any other
//--- live-confidence consumer) - see g_LiveAISignedConfidence in Variables\ConfidenceBridge.mqh.
//--- Cheap: SignedAIConfidence() just reads the already-computed dPrevSignal.
//--- ENSEMBLE: the AVERAGE across members, not this member's own reading. Every member ran this line
//--- unconditionally, every tick, so the global was simply whichever member's OnTick happened to run
//--- last. So on a four-model chart an LSTM entry could have its stop moved on the Perceptron's
//--- opinion alone, purely by scheduling order. Not the vote, not a weighted blend - an arbitrary
//--- member. (User-identified 2026-08-17.) The only consumer left is the intelligent trailing stop;
//--- the AI early-exit route that was the other one is gone with the vote-layer simplification.
//--- Averaging matches how the ensemble actually trades: the open decision is the weighted-average
//--- vote, and a member that abstains contributes 0 and dilutes, exactly as it does there. Members
//--- still training read 0 from SignedAIConfidence(), so a half-trained ensemble reads WEAKER rather
//--- than louder, which is the safe direction for an exit trigger.
//--- Currently latent, and worth keeping that way deliberately: Signal_ThresholdClose ships Disabled (101,
//--- unreachable on both scales it drives) and TrailingStrategy is off, so neither consumer fires
//--- today. This is fixed now precisely because the plan is to enable vote exits once the models are
//--- accurate - at which point a scheduling-order exit would be actively harmful and very hard to see.
//--- PUBLISH ONLY THIS SIGNAL'S OWN VOTE. Combining is the orchestrator's job, never a member's - see
//--- the vote board in Variables\ConfidenceBridge.mqh for the scheduling bug this replaces and for why
//--- "have a member average its siblings" was the wrong shape of fix in a codebase whose whole point is
//--- that signals do not reach into each other. A solo AI signal owns slot 0 and the aggregate is then
//--- just its own value, so the non-ensemble path is unchanged.
PublishAIVote(m_ensembleMember ? m_ensembleIndex : 0, SignedAIConfidence());
datetime lastBarDate = (datetime)SeriesInfoInteger(m_symbol.Name(), m_period, SERIES_LASTBAR_DATE);
//--- A failed lookup (0) must not silently read as "dtStudied is already caught up, nothing
//--- pending" - that would freeze this function into never re-triggering training/signal refresh
//--- again until some other path happens to bump dtStudied.
bool newBarPending = (dPrevSignal == -2 || lastBarDate <= 0 || ((m_inferenceOnly ? m_lastBarTime : dtStudied) < lastBarDate));
//--- A MODEL THAT HAS NOT FINISHED TRAINING IS ALWAYS PENDING. Gating them on the watermark
//--- meant training could only advance when a new BAR closed. On H1 that is one 120ms chunk per
//--- hour. A POST-TRAINING WALK IS ALSO PENDING.
bool postTrainWalkPending = (m_simOosRunActive || m_dbBackfillActive);
bool trainingPending = !(m_trainingComplete || m_inferenceOnly) || postTrainWalkPending;
//--- m_inferenceOnly (single backtest) takes the converged/inference branch even if the seeded model
//--- wasn't flagged complete, so a backtest never drops into Train()'s era loop - see m_inferenceOnly.
if((m_trainingComplete || m_inferenceOnly) && !m_trainingStopRequested && !m_trainRunActive &&
!postTrainWalkPending)
{
if(newBarPending)
RefreshConvergedSignal();
}
else
{
//--- Lost-event watchdog: an armed event that never arrived (chart-event queue overflow) would
//--- otherwise leave bEventStudy true forever and silently stall training - the accidental rescue
//--- (any sibling's event clearing this flag) went away with the per-instance ids. See
//--- STUDY_EVENT_LOST_MS for why a false trip is not a realistic concern.
if(bEventStudy && GetTickCount() - m_studyArmedTick > STUDY_EVENT_LOST_MS)
{
Print(ID + ": study event armed " + IntegerToString(STUDY_EVENT_LOST_MS / 1000) +
"s ago never arrived (chart-event queue overflow?) - re-arming.");
bEventStudy = false;
}
if(!m_trainingStopRequested && !bEventStudy && (newBarPending || trainingPending))
ArmStudyEvent((long)MathMax(0, MathMin(iTime(m_symbol.Name(), PERIOD_CURRENT, (int)(100 * Net.recentAverageSmoothingFactor * (m_trainingComplete ? 1 : 10))), dtStudied)), "New Bar");
}
//--- Train() (see its declaration comment) now yields every ~TRAIN_TIME_BUDGET_MS instead of
//--- blocking for a whole era, so while a run is active this per-tick line would otherwise
//--- overwrite Train()'s own full-detail status label on every single tick between chunks -
//--- flickering between the two instead of showing one steady picture.
if(m_ensembleMember && EnsembleEraBarrierHolds())
return;
if(!m_trainRunActive)
{
//--- Compact, accurate end-state text.
bool onlineActive = m_enableOnlineLearning && !m_inferenceOnly
&& !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION) && !MQLInfoInteger(MQL_FORWARD)
&& CheckPointer(Net) != POINTER_INVALID && !Net.CpuInference();
//--- Simple end-state panel (default, VerboseMode off): plain-language status + the model's
//--- compounded/persistent Buy/Sell win-rate (directional accuracy, Neutral excluded - persisted in
//--- .stats WST5, so it survives a fresh chart reload and is meaningful the moment a drop-and-go user
//--- attaches the EA) + the current call. The verbose era/forecast dump below stays for power users.
if(!VerboseMode)
{
string statusPlain;
if(m_trainingComplete)
statusPlain = onlineActive ? "Live - learning from new bars" : "Ready for live trading";
else if(m_trainingStopRequested)
statusPlain = "Paused - progress saved";
else if(m_trainingPaused)
statusPlain = "Paused";
else
statusPlain = "Getting ready...";
ENUM_SIGNAL liveSig = DoubleToSignal(dPrevSignal);
string liveSigPlain = (liveSig == Buy) ? "Buy" : (liveSig == Sell) ? "Sell" : "Neutral (no trade)";
string simpleLive = DisplayName() + " - " + statusPlain + "\n";
//--- Only show the accuracy line once at least one signal has been validated (compounded counts
//--- persist across restarts, so a deployed model shows real numbers immediately, not "measuring").
if(m_cumIsTotal > 0 || m_cumOosTotal > 0)
simpleLive += ComputeCompoundedAccuracyLine() + "\n";
//--- The ensemble headline is the FIRST line, so lead with the signal - on the combined
//--- panel each member's one line must answer "what is this model saying right now".
if(m_ensembleMember)
{
simpleLive = statusPlain + " -> " + liveSigPlain;
//--- one-line accuracy on the member headline (user request 2026-08-16: the solo panel
//--- shows accuracy, the ensemble panel did not).
if(m_cumOosTotal > 0)
{
double slBeH = 0.0, tpBeH = 0.0;
BarrierMultiples(slBeH, tpBeH);
simpleLive += StringFormat(" | win %d%%", (int)MathRound(m_cumOosCorrect * 100.0 / m_cumOosTotal));
if(slBeH > 0.0 && tpBeH > 0.0)
simpleLive += StringFormat(" (need %d%%)", (int)MathRound(100.0 * slBeH / (slBeH + tpBeH)));
}
}
PublishStatus(simpleLive);
return;
}
string completeText = onlineActive ? "Complete - live (adapting to new bars)" : "Complete - ready for live (inference)";
string trainingState = m_trainingStopRequested
? (m_trainingComplete ? completeText : "Stopped - resumable (weights kept)")
: (m_trainingPaused ? "Paused" : (m_trainingComplete ? completeText : "In progress"));
//--- same "Forecast: <signal> -> <value>" line the active training loop's status label ends on
//--- (see the classLine-terminated StringFormat below), instead of a raw bEventStudy/dPrevSignal/
//--- dtStudied debug dump - this is what stays on screen once training stops/pauses/completes.
PublishStatus(StringFormat(
ID + " : Era %d -> Training %s\n" +
"Forecast: %s -> %.2f",
m_eraCount, trainingState,
EnumToString(DoubleToSignal(dPrevSignal)), dPrevSignal));
}
}
//+------------------------------------------------------------------+
//| Timer-driven equivalent of OnTickHandler()'s scheduling, called |
//| from Warrior_EA.mq5's always-on OnTimer() so training keeps |
//| progressing purely on wall-clock time - no dependency on ticks, |
//| which simply don't arrive while the market is closed. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::PollTraining(void)
{
//--- Drain a slice of the queued chart-arrow restore FIRST, and skip training work on any tick
//--- where restoring is still in flight.
//--- STOP BEFORE ANY OF IT. Drawing arrows for a chart that is unloading is the worst case of the
//--- three: it lengthens the very sweep that has to finish.
if(ShutdownRequested())
return;
if(m_arrowRestorePending)
{
AdvanceChartSignalRestore();
return;
}
//--- Same one-thread reasoning as the arrow restore above: a manual rescan (Show Signals) also competes
//--- for the single MQL5 thread, and its per-bar feedForward is real compute rather than a cheap object
//--- write, so it must finish its own slices before training resumes rather than interleaving with it.
if(m_rescanPending)
{
AdvanceChartSignalRescan();
return;
}
if(m_isInitialized)
ScheduleTrainingIfNeeded();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::OnChartEventHandler(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
//--- Match THIS instance's id only. See STUDY_EVENT_ID_BASE (ExpertSignalAIBase.mqh) for the
//--- full failure shape.
if(id == CHARTEVENT_CUSTOM + m_studyEventId)
{
//--- The study event IS the training driver, so a queued one landing after the stop request would
//--- open a whole era inside the teardown window. Checked here as well as in Warrior_EA.mq5's
//--- OnChartEvent because CExpertCustom re-posts these between members.
if(ShutdownRequested())
return;
TuneIndicatorsAndTrain(lparam);
bEventStudy = false;
OnTickHandler();
}
}
//--- Set when a start is refused for a reason no retry can change. OnInit's retry loop reads it so
//--- the operator's LAST log line names the cause instead of "Failed to initialize Indicators".
string g_initFatalReason = "";
//+------------------------------------------------------------------+
//| Claim m_activeFileName for this chart, terminal-wide. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase::AcquireConfigLock(void)
{
//--- FNV-1a over the resolved filename: every retrain-affecting input is already folded into that
//--- name, so equal names mean genuinely equal configs and nothing else has to be compared. Hashed
//--- because MQL5 caps global-variable names at 63 characters and the path alone exceeds that.
uint h = 2166136261;
int len = StringLen(m_activeFileName);
for(int i = 0; i < len; i++)
{
h ^= (uint)StringGetCharacter(m_activeFileName, i);
h *= 16777619;
}
string name = "WarriorAI_" + m_id + "_" + StringFormat("%08x", h);
long self = ChartID();
//--- Atomic: true means it did not exist and is now ours.
if(GlobalVariableTemp(name))
{
GlobalVariableSet(name, (double)self);
m_configLockName = name;
return true;
}
long owner = (long)GlobalVariableGet(name);
//--- Our own entry: this chart is re-initializing after a parameter change or a recompile whose
//--- OnDeinit never reached ReleaseConfigLock(). Reclaim it instead of refusing to start.
if(owner == self)
{
m_configLockName = name;
return true;
}
//--- Owner recorded but its chart no longer runs an expert - take the claim over. owner == 0 is
//--- deliberately NOT treated as stale: it means another instance created the variable microseconds
//--- ago and has not stamped its id yet, which is a live claim, not a dead one.
bool ownerAlive = false;
if(owner != 0)
{
long id = ChartFirst();
while(id >= 0)
{
if(id == owner)
{
ownerAlive = (StringLen(ChartGetString(id, CHART_EXPERT_NAME)) > 0);
break;
}
id = ChartNext(id);
}
}
if(owner != 0 && !ownerAlive)
{
GlobalVariableSet(name, (double)self);
m_configLockName = name;
return true;
}
Print(ID + ": REFUSED to start - another chart is already training this exact configuration. Both" +
" would save into the same files (" + m_activeFileName + ".nnw plus its .cfg/.stats/checkpoints)" +
" and overwrite each other's progress with no error reported anywhere. Owner: " +
(owner != 0 ? "chart " + IntegerToString(owner) + " (" + ChartSymbol(owner) + " " +
EnumToString((ENUM_TIMEFRAMES)ChartPeriod(owner)) + ")" : "another chart, still initializing") +
". Change the enabled NN set (Use_MLP/Use_CONV/Use_LSTM/Use_CONVLSTM) or any retrain-affecting" +
" input on THIS chart so it trains its own model, or remove one of the two charts. Note the" +
" private build defaults ALL FOUR direction NNs on - two same-symbol charts left at their" +
" defaults land here.");
g_initFatalReason = "another chart already owns this configuration (" + m_activeFileName + ")";
return false;
}
//+------------------------------------------------------------------+
//| Drop this instance's claim (see AcquireConfigLock). |
//+------------------------------------------------------------------+
void CExpertSignalAIBase::ReleaseConfigLock(void)
{
if(StringLen(m_configLockName) == 0)
return;
//--- Only delete a claim we still hold: if a later instance took this entry over via the stale-owner
//--- path above, deleting it here would silently hand the config to a third chart.
if((long)GlobalVariableGet(m_configLockName) == ChartID())
GlobalVariableDel(m_configLockName);
m_configLockName = "";
}
#endif