forked from animatedread/Warrior_EA
SP500 resumed converged at era 136 with its tier ladder correctly restored and still swept 4999 bars reporting "0 had a snapshot, drew 0 arrow(s)" while the other five charts drew 221-312. HasDemonstratedEdge() - added with the no-skill exclusion - compares m_eraStatPrecPct against m_eraStatChancePct. Both are written once per era by EnsembleStashEraStats. A converged model runs no eras, so after a restart both sat at their -1 ctor defaults, every member was ruled no-skill, ReconstructionWeight() returned 0 for all four, and the overlay divisor was zero on every bar. Exactly the failure the WST7 ladder persistence fixed one level down: the ladder says how much a member votes, this says whether it may. RankTiersFromOos already computes the pair (pooled holdout precision and the zero-skill reference rate) and now records it as the CERTIFIED edge. That path is reached by the era end AND by the deployed replay, which is the only measurement a converged model will ever make. Persisted as WST8; HasDemonstratedEdge() prefers the era pair and falls back to it. The census line also had to be fixed: it reported "NOT ONE of those bars had a single member snapshot ... no enrolled member has published m_overlaySigSnap" for a condition that was purely a skill verdict. The snapshots were there. It now counts the two causes separately and names the one that fired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
707 lines
44 KiB
MQL5
707 lines
44 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| Model .stats / .cfg sidecars, CPU-inference validation, net load |
|
|
//| retry. STATELESS - every field these methods touch is shared |
|
|
//| elsewhere in the signal (grep-verified), unlike CChartUI's |
|
|
//| arrow-restore/rescan queues, so this collaborator owns nothing |
|
|
//| but the borrowed view. Every read/write below is a pure |
|
|
//| relocation of Expert\AIBase\Persistence.mqh's original bodies - |
|
|
//| same order, same conditionals, no logic changes. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_PERSISTENCE_MODELPERSISTENCE_MQH
|
|
#define WARRIOR_PERSISTENCE_MODELPERSISTENCE_MQH
|
|
class CModelPersistence
|
|
{
|
|
private:
|
|
CPersistenceView *m_view; // BORROWED - the signal owns the adapter, not the reverse
|
|
public:
|
|
CModelPersistence(void) : m_view(NULL) { }
|
|
void Bind(CPersistenceView *view) { m_view = view; }
|
|
|
|
void EnforceTopologyContract(void);
|
|
bool SaveModelStats(string fileName, bool common);
|
|
bool LoadModelStats(string fileName, bool common);
|
|
bool ValidateCpuInference(void);
|
|
bool SaveTopologyConfiguration(string fileName, int initialNeuronsCount, int hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int historyBars, int outputNeuronsCount, int neuronsCount, int studyPeriod, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int convFilterCount, int lstmHiddenSize, bool common);
|
|
bool LoadAndCompareTopologyConfiguration(string fileName, int &initialNeuronsCount, int &hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int &historyBars, int outputNeuronsCount, int neuronsCount, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int &convFilterCount, int &lstmHiddenSize, bool common);
|
|
string ReadAltDataPinFromCfg(void);
|
|
bool LoadNetWithRetry(double &indicatorParams[]);
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| Re-assert the parts of a just-loaded net that the CODE owns but |
|
|
//| the FILE also stores. See OutputLayerActivation()'s declaration |
|
|
//| comment and CNet::EnforceOutputActivation() (AI\Network.mqh). |
|
|
//+------------------------------------------------------------------+
|
|
void CModelPersistence::EnforceTopologyContract(void)
|
|
{
|
|
if(!m_view.NetLoaded())
|
|
return;
|
|
//--- Stale conv receptive field. Unlike the activation below this cannot be repaired in place: the
|
|
//--- conv weight block is (window+1)*window_out, so a different window is a different tensor. Flag it
|
|
//--- and let the caller retrain - see m_topologySuperseded's use in InitNeuralNetwork.
|
|
if(m_view.UsesConvStage())
|
|
{
|
|
uint loadedWindow = m_view.NetFirstConvWindow();
|
|
uint intendedWindow = (uint)(m_view.ConvReceptiveFieldBars() * m_view.NeuronsCount());
|
|
if(loadedWindow > 0 && loadedWindow != intendedWindow)
|
|
{
|
|
m_view.SetTopologySuperseded(true);
|
|
Print(m_view.Id() + ": SUPERSEDED architecture on disk - the saved model's conv receptive field is " +
|
|
IntegerToString((int)loadedWindow) + " (" + IntegerToString((int)(loadedWindow / (uint)MathMax(1, m_view.NeuronsCount()))) +
|
|
" bars), this build specifies " + IntegerToString((int)intendedWindow) + " (" +
|
|
IntegerToString(m_view.ConvReceptiveFieldBars()) + " bars). The conv weight tensor is a different" +
|
|
" shape, so this cannot be repaired in place - retraining from era 0.");
|
|
}
|
|
}
|
|
ENUM_ACTIVATION intended = m_view.OutputLayerActivation();
|
|
ENUM_ACTIVATION stale = intended;
|
|
if(!m_view.NetEnforceOutputActivation(intended, stale))
|
|
return;
|
|
Print(m_view.Id() + ": REPAIRED loaded model - output layer activation was " + ActivationName(stale) +
|
|
" on disk, topology specifies " + ActivationName(intended) +
|
|
". The saved file was produced by an older build; it has been corrected in memory and the next" +
|
|
" save will persist the correction. If training looks wrong from here, reset weights and retrain -" +
|
|
" these weights were learned against the stale head.");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Persist/restore the calibration state that must survive a restart |
|
|
//| for live trading to behave like training: the true class priors |
|
|
//| and m_confidenceCalScale. Same FILE_COMMON/tester write-guard as |
|
|
//| CNet::Save so a backtest never overwrites the shared production |
|
|
//| stats. Versioned/magic-prefixed; a mismatch is treated as absent. |
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::SaveModelStats(string fileName, bool common)
|
|
{
|
|
//--- mirror CNet::Save's guard: shared production stats are never written from inside a backtest
|
|
if(common && (MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_FORWARD)))
|
|
return true;
|
|
//--- Staged through a temp file + atomic rename (System\AtomicFile.mqh).
|
|
int statsCommonFlag = (common ? FILE_COMMON : 0);
|
|
string statsTmpName = "";
|
|
int handle = AtomicWriteBegin(fileName + ".stats", statsCommonFlag, statsTmpName);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print(__FUNCTION__ + ": FileOpen failed for " + statsTmpName + ", error " + IntegerToString(GetLastError()) +
|
|
" - calibration/online-learning state not persisted.");
|
|
return false;
|
|
}
|
|
//--- WST6 has the SAME field layout as WST5 - the bump exists to invalidate stale IS counters,
|
|
//--- whose MEANING changed: they used to count every oversampled occurrence, now they count each
|
|
//--- bar once.
|
|
bool ok = true;
|
|
if(FileWriteInteger(handle, 0x57535438) <= 0) // 'WST8' magic/version (WST8 = +the member's certified precision/chance pair, WST7 = +the ensemble's certified record, WST6 = IS-counter meaning fixed, WST5 = +compounded IS/OOS counts, WST4 = +live reliability, WST3 = +online state, WST2 = +CPU-marker, WST1 = base)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.PriorBuy()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.PriorSell()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.PriorNeutral()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.ConfidenceCalScale()) <= 0)
|
|
ok = false;
|
|
//--- CPU-inference-safe marker (see ValidateCpuInference): gates whether an inference-only backtest
|
|
//--- of this model may run DLL-free. Appended after the v1 fields so a v1 reader stops cleanly before it.
|
|
if(ok && FileWriteInteger(handle, m_view.MqlInferenceValidated() ? 1 : 0) <= 0)
|
|
ok = false;
|
|
//--- Online continual-learning state (WST3, see OnlineLearnStep) - the bar-time watermark of the
|
|
//--- newest bar already learned from, the rolling guardrail accuracy, and the cumulative update count.
|
|
//--- Appended after the WST2 fields so a WST2 reader stops cleanly before them.
|
|
if(ok && FileWriteLong(handle, (long)m_view.OnlineLearnedUpToTime()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.OnlineRollingAcc()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteLong(handle, m_view.OnlineSamples()) <= 0)
|
|
ok = false;
|
|
//--- Deployed model's last-measured OOS reliability (WST4) - the live status panel shows this as the
|
|
//--- "signal hit-rate" so a freshly-reloaded, inference-only model still reports what to expect live
|
|
//--- (these members are computed only during training, so without persistence they read n/a on reload).
|
|
//--- Appended after the WST3 fields so a WST3 reader stops cleanly before them.
|
|
if(ok && FileWriteInteger(handle, m_view.LastBuyFiredPrecPct()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.LastSellFiredPrecPct()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.LastBuyRecallPct()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.LastSellRecallPct()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.LastBuyFired()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.LastSellFired()) <= 0)
|
|
ok = false;
|
|
//--- Compounded, persistent IS/OOS accuracy counts (WST5) - see m_cumIsCorrect. Carried across
|
|
//--- restarts so the panel's accuracy keeps compounding instead of restarting each session.
|
|
//--- Appended after the WST4 fields so a WST4 reader stops cleanly before them.
|
|
if(ok && FileWriteLong(handle, m_view.CumIsCorrect()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteLong(handle, m_view.CumIsTotal()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteLong(handle, m_view.CumOosCorrect()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteLong(handle, m_view.CumOosTotal()) <= 0)
|
|
ok = false;
|
|
//--- THE TIER LADDER (WST7), AND IT IS THE MOST IMPORTANT THING IN THIS FILE.
|
|
//---
|
|
//--- These four weights plus the module trust weight are the model's VOTE. They are produced only
|
|
//--- by a completed pass 3 (RankTiersFromOos), and until they exist LiveVoteContribution() returns
|
|
//--- 0 for every call - deliberately, because the constructor's stock 25/50/75/100 ladder is the
|
|
//--- wrong UNIT (a win-rate currency since 2026-08-18), not a weak opinion, and one unranked member
|
|
//--- would drag the whole ensemble over any threshold.
|
|
//---
|
|
//--- That silencing is right while training and catastrophic on a resume. A CONVERGED model runs no
|
|
//--- further passes, so every restart left it permanently mute: no live vote, no reconstruction
|
|
//--- weight, an overlay divisor of zero on every bar, and therefore no arrows and no aggregate win
|
|
//--- rate. Diagnosed 2026-08-25 from six H4 charts that read "0 vote/4 flat, peak 0.0%" and swept
|
|
//--- 4999 bars finding "0 had a snapshot" - with the models resumed at era 70/71 and every rescan
|
|
//--- reporting ~2700 Buy / ~2200 Sell. The calls were there; nothing was allowed to count them.
|
|
if(ok && FileWriteInteger(handle, m_view.TierWeight(0)) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.TierWeight(1)) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.TierWeight(2)) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.TierWeight(3)) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.ModuleTrustWeight()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, m_view.TiersSelfRanked() ? 1 : 0) <= 0)
|
|
ok = false;
|
|
//--- THE ENSEMBLE'S CERTIFIED RECORD (WST7). Everything below is CHART-scoped, not member-scoped -
|
|
//--- every member writes the same values into its own .stats, and load takes the most complete
|
|
//--- copy. That redundancy is deliberate: there is no ensemble-owned file, and inventing one would
|
|
//--- need its own key, its own atomic write and its own reset-weights wipe, all of which this
|
|
//--- sidecar already has.
|
|
//---
|
|
//--- WHY IT HAS TO BE PERSISTED AT ALL: these were session globals written once per era at pass-3
|
|
//--- completion. A DEPLOYED ensemble runs no further eras, so on every restart the aggregate win
|
|
//--- rate, the deploy verdict and the panel line that reports them were lost and could never be
|
|
//--- regenerated - the panel then fell back to the per-member rows (user report 2026-08-25).
|
|
//---
|
|
//--- The vote threshold rides along because every number here is CONDITIONAL ON IT: `fired` counts
|
|
//--- only bars where |vote| cleared it. Restoring these under a different threshold would report a
|
|
//--- win rate for a strategy the chart is no longer running.
|
|
if(ok && FileWriteLong(handle, g_ensCumOosCorrect) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteLong(handle, g_ensCumOosTotal) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, g_ensBestPrecPct) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, g_ensBestChancePct) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, g_ensBestCalls) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, g_ensDeployApproved ? 1 : 0) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, g_ensembleVoteThreshold) <= 0)
|
|
ok = false;
|
|
//--- THIS MEMBER'S CERTIFIED PRECISION AND CHANCE RATE (WST8) - member-scoped, unlike the block
|
|
//--- above. The SAME failure the tier ladder had, discovered the same way: SP500 resumed converged
|
|
//--- at era 136 with its ladder restored and still swept 4999 bars reporting "0 had a snapshot,
|
|
//--- drew 0 arrow(s)", because HasDemonstratedEdge() (added 2026-08-26 with the no-skill exclusion)
|
|
//--- compares two era-only doubles that a converged model never recomputes. Both sat at -1, so
|
|
//--- every member was ruled no-skill, ReconstructionWeight() returned 0 and the overlay divisor was
|
|
//--- zero on every bar. Persisting the ladder alone was not enough - the ladder says HOW MUCH a
|
|
//--- member votes, this says WHETHER IT MAY.
|
|
if(ok && FileWriteDouble(handle, m_view.CertifiedPrecPct()) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_view.CertifiedChancePct()) <= 0)
|
|
ok = false;
|
|
//--- A partial write (disk full mid-write) now discards the temp and leaves the previous good
|
|
//--- .stats in place, instead of publishing a truncated one that reads back as all-zero
|
|
//--- calibration state.
|
|
return AtomicWriteEnd(handle, fileName + ".stats", statsTmpName, statsCommonFlag, ok, __FUNCTION__);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::LoadModelStats(string fileName, bool common)
|
|
{
|
|
if(!FileIsExist(fileName + ".stats", common ? FILE_COMMON : 0))
|
|
return false;
|
|
//--- share flags: read-only, must not fail just because another process holds the file - see CopySharedFile().
|
|
int handle = FileOpen(fileName + ".stats", (common ? FILE_COMMON : 0) | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
|
|
if(handle == INVALID_HANDLE)
|
|
return false;
|
|
int magic = FileReadInteger(handle);
|
|
if(magic != 0x57535431 && magic != 0x57535432 && magic != 0x57535433 && magic != 0x57535434 &&
|
|
magic != 0x57535435 && magic != 0x57535436 && magic != 0x57535437 && magic != 0x57535438)
|
|
{
|
|
FileClose(handle);
|
|
return false;
|
|
}
|
|
m_view.SetPriorBuy(FileReadDouble(handle));
|
|
m_view.SetPriorSell(FileReadDouble(handle));
|
|
m_view.SetPriorNeutral(FileReadDouble(handle));
|
|
m_view.SetConfidenceCalScale(FileReadDouble(handle));
|
|
//--- THE VERSION TESTS ARE ">=", NOT AN OR-CHAIN. The magics are the ASCII bytes 'WST1'..'WST7',
|
|
//--- so they increase monotonically and "this field exists from vN onward" is exactly an ordering
|
|
//--- question. The chain this replaces had to be edited in five places to add one version, and a
|
|
//--- missed one would silently read the NEXT field's bytes into this one - a corruption that reads
|
|
//--- back as plausible numbers rather than as a failure. The accepted set is already validated by
|
|
//--- the magic check above, so ">=" cannot admit an unknown format here.
|
|
//--- v2+ appended the CPU-inference-safe marker; v1 files predate it (treated as not-yet-validated so
|
|
//--- the model stays on the DLL path until re-deployed by a build that runs ValidateCpuInference).
|
|
m_view.SetMqlInferenceValidated((magic >= 0x57535432) ? (FileReadInteger(handle) != 0) : false);
|
|
//--- v3 appended the online continual-learning state. Older files predate it: leave the watermark at 0
|
|
//--- (OnlineLearnStep anchors it to the current frontier on first run - no retroactive backprop) and
|
|
//--- the rolling accuracy at -1 (re-seeded from the deploy baseline on the first update).
|
|
if(magic >= 0x57535433)
|
|
{
|
|
m_view.SetOnlineLearnedUpToTime((datetime)FileReadLong(handle));
|
|
m_view.SetOnlineRollingAcc(FileReadDouble(handle));
|
|
m_view.SetOnlineSamples(FileReadLong(handle));
|
|
}
|
|
//--- v4 appended the deployed model's last-measured OOS reliability (for the live status panel). Older
|
|
//--- files predate it: the members keep their -1 / 0 ctor defaults, so the panel shows no hit-rate line
|
|
//--- until the model is re-trained (or re-deployed) by a WST4+ build - exactly the pre-persistence behaviour.
|
|
if(magic >= 0x57535434)
|
|
{
|
|
m_view.SetLastBuyFiredPrecPct(FileReadInteger(handle));
|
|
m_view.SetLastSellFiredPrecPct(FileReadInteger(handle));
|
|
m_view.SetLastBuyRecallPct(FileReadInteger(handle));
|
|
m_view.SetLastSellRecallPct(FileReadInteger(handle));
|
|
m_view.SetLastBuyFired(FileReadInteger(handle));
|
|
m_view.SetLastSellFired(FileReadInteger(handle));
|
|
}
|
|
//--- v5 appended the compounded/persistent IS/OOS accuracy counts (see m_cumIsCorrect). Older files
|
|
//--- predate it: the counts keep their 0 ctor defaults, so the panel shows "measuring" until the next
|
|
//--- era scores signals - then it resumes compounding from there.
|
|
if(magic >= 0x57535435)
|
|
{
|
|
m_view.SetCumIsCorrect(FileReadLong(handle));
|
|
m_view.SetCumIsTotal(FileReadLong(handle));
|
|
m_view.SetCumOosCorrect(FileReadLong(handle));
|
|
m_view.SetCumOosTotal(FileReadLong(handle));
|
|
//--- A WST5 file's IS pair counted every OVERSAMPLED OCCURRENCE, so it was measured against a
|
|
//--- ~58%-directional queue instead of the real ~6% distribution - not comparable with the
|
|
//--- OOS pair beside it, and the source of the "IS 77% / OOS 12%, looks like overfitting"
|
|
//--- reading.
|
|
if(magic == 0x57535435)
|
|
{
|
|
m_view.SetCumIsCorrect(0);
|
|
m_view.SetCumIsTotal(0);
|
|
}
|
|
}
|
|
//--- v7 appended the ENSEMBLE's certified record - chart-scoped, mirrored into every member's file.
|
|
//--- See the matching write for why it must be persisted at all.
|
|
if(magic >= 0x57535437)
|
|
{
|
|
//--- THE TIER LADDER FIRST, in write order. Restoring it is what lets a resumed converged model
|
|
//--- vote at all - see the matching write for the failure it fixes. Adopted UNCONDITIONALLY:
|
|
//--- these weights belong to the .nnw sitting beside this file, they were measured on that
|
|
//--- model's own holdout, and the alternative is not a safer number but silence.
|
|
int t0 = FileReadInteger(handle);
|
|
int t1 = FileReadInteger(handle);
|
|
int t2 = FileReadInteger(handle);
|
|
int t3 = FileReadInteger(handle);
|
|
double moduleW = FileReadDouble(handle);
|
|
bool ranked = (FileReadInteger(handle) != 0);
|
|
//--- ...but only when the file actually carries a ranked ladder. A model that was saved BEFORE
|
|
//--- its first pass 3 stored the stock 25/50/75/100, and adopting that as if measured is the
|
|
//--- precise error the self-ranked flag exists to prevent.
|
|
if(ranked)
|
|
{
|
|
m_view.SetTierWeight(0, t0);
|
|
m_view.SetTierWeight(1, t1);
|
|
m_view.SetTierWeight(2, t2);
|
|
m_view.SetTierWeight(3, t3);
|
|
if(moduleW > 0.0 && moduleW <= 1.0)
|
|
m_view.SetModuleTrustWeight(moduleW);
|
|
m_view.SetTiersSelfRanked(true);
|
|
Print(m_view.Id() + ": restored the measured tier ladder from .stats -> T0=" + IntegerToString(t0) +
|
|
" T1=" + IntegerToString(t1) + " T2=" + IntegerToString(t2) + " T3=" + IntegerToString(t3) +
|
|
" | module weight " + DoubleToString(moduleW, 2) +
|
|
". This model can vote immediately; without it a converged model stays silent until it retrains.");
|
|
}
|
|
long ensCorrect = FileReadLong(handle);
|
|
long ensTotal = FileReadLong(handle);
|
|
double ensPrec = FileReadDouble(handle);
|
|
double ensChance = FileReadDouble(handle);
|
|
int ensCalls = FileReadInteger(handle);
|
|
bool ensDeploy = (FileReadInteger(handle) != 0);
|
|
double ensThresh = FileReadDouble(handle);
|
|
//--- THE THRESHOLD IS NOW ADOPTED, NOT COMPARED (2026-08-26). It used to be compared, and that
|
|
//--- was right while it was an OPERATOR'S INPUT: a record built at 25% said nothing about a
|
|
//--- chart now running 15%, so it was dropped and the next era rebuilt it. Since the threshold
|
|
//--- became a DERIVED, PINNED property of the checkpoint, the comparison inverted its own
|
|
//--- meaning - at load time g_ensembleVoteThreshold is still the Signal_ThresholdOpen SEED, so
|
|
//--- the stored value never matches and the record is always dropped. Two things died with it,
|
|
//--- both silently:
|
|
//--- * g_ensDeployApproved. A DEPLOYED ensemble came back as a training one on every restart.
|
|
//--- * the pinned threshold itself. PublishVoteThreshold() only fires on a positive
|
|
//--- g_ensDerivedThreshold, so a deployed chart would have traded the .chr seed instead of
|
|
//--- the rung its deploy was certified at - certified != traded, again.
|
|
//--- Adopting restores threshold, counts and deploy flag TOGETHER, which is the only coherent
|
|
//--- state: the counts were conditional on that threshold, and it is stored beside them for
|
|
//--- exactly that reason. Same doctrine as the .cfg topology - adopt what the model was
|
|
//--- certified with, never re-derive it underneath a checkpoint.
|
|
//---
|
|
//--- THE MOST COMPLETE COPY STILL WINS. Every member writes this same record into its own
|
|
//--- .stats at slightly different moments, so a member whose file was written an era earlier
|
|
//--- must not overwrite a fuller one loaded a moment ago. Ordering by total fired bars is
|
|
//--- monotone (the counters only accumulate), which is what makes load order not matter.
|
|
if(ensThresh > 0.0 && ensTotal >= g_ensCumOosTotal)
|
|
{
|
|
g_ensCumOosCorrect = ensCorrect;
|
|
g_ensCumOosTotal = ensTotal;
|
|
g_ensBestPrecPct = ensPrec;
|
|
g_ensBestChancePct = ensChance;
|
|
g_ensBestCalls = ensCalls;
|
|
g_ensDeployApproved = ensDeploy;
|
|
g_ensembleVoteThreshold = ensThresh;
|
|
g_ensDerivedThreshold = ensThresh;
|
|
PrintFormat("%s: restored the ensemble record - %d%% threshold (pinned), %d calls, %d%% win"
|
|
" rate, deploy %s. The chart trades the rung its checkpoint was certified at.",
|
|
__FUNCTION__, (int)MathRound(ensThresh), (int)ensTotal,
|
|
(ensTotal > 0 ? (int)MathRound(100.0 * ensCorrect / ensTotal) : 0),
|
|
(ensDeploy ? "APPROVED" : "not yet approved"));
|
|
}
|
|
}
|
|
//--- v8 appended THIS MEMBER's certified precision/chance pair - what HasDemonstratedEdge() reads,
|
|
//--- and therefore whether this member is allowed into the vote at all. Older files predate it: the
|
|
//--- pair keeps its -1 defaults and the member stays no-skill until its next era measures one,
|
|
//--- which for a CONVERGED model is never - so a WST7 file must be re-saved once by this build.
|
|
if(magic >= 0x57535438)
|
|
{
|
|
double certPrec = FileReadDouble(handle);
|
|
double certChance = FileReadDouble(handle);
|
|
if(certPrec >= 0.0 && certChance >= 0.0)
|
|
{
|
|
m_view.SetCertifiedEdge(certPrec, certChance);
|
|
PrintVerbose(m_view.Id() + ": restored the certified edge from .stats -> " +
|
|
DoubleToString(certPrec, 1) + "% precision against a " + DoubleToString(certChance, 1) +
|
|
"% chance rate (" + (certPrec > certChance ? "MAY VOTE" : "no skill - excluded from the vote") +
|
|
"). Without it a resumed converged model is ruled no-skill and the overlay divisor is zero.");
|
|
}
|
|
}
|
|
FileClose(handle);
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Deploy-time self-check (chart only, where a compute backend |
|
|
//| exists): run the just-saved deployed model through both the |
|
|
//| backend Net and a throwaway pure-MQL5 clone (CNet::SetCpuInference|
|
|
//| loaded from the same .nnw) on one real input window, and return |
|
|
//| true only if their outputs match within CPU_INFERENCE_MAX_DIFF. |
|
|
//| This is what lets an inference-only backtest run DLL-free; any |
|
|
//| error, size mismatch, or a not-yet-ported architecture (conv/LSTM |
|
|
//| CPU load fails) returns false -> the model stays on the DLL path. |
|
|
//| The Net-pointer/throwaway-clone core is consolidated as ONE view |
|
|
//| call (PersistRunCpuInferenceSelfCheck) rather than field-by-field -|
|
|
//| it is irreducible pointer/object work, not signal state. |
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::ValidateCpuInference(void)
|
|
{
|
|
//--- Chart-only: needs a real backend to compare against, and only the shared production model (not a
|
|
//--- per-agent optimization cache) is ever seeded into a buyer's inference-only backtest.
|
|
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
|
|
return false;
|
|
double maxDiff = DBL_MAX;
|
|
bool pass = m_view.RunCpuInferenceSelfCheck(maxDiff);
|
|
PrintVerbose(m_view.Id() + ": CPU-inference validation " + (pass ? "PASSED - backtests may run DLL-free" :
|
|
"FAILED - backtests keep using the DLL") + " (max |delta| = " +
|
|
(maxDiff == DBL_MAX ? "n/a" : DoubleToString(maxDiff, 8)) + ", tol " +
|
|
DoubleToString(CPU_INFERENCE_MAX_DIFF, 8) + ")");
|
|
return pass;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::SaveTopologyConfiguration(string fileName, int initialNeuronsCount, int hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int historyBars, int outputNeuronsCount, int neuronsCount, int studyPeriod, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int convFilterCount, int lstmHiddenSize, bool common)
|
|
{
|
|
string configFileName = fileName + ".cfg";
|
|
//--- Staged through a temp file + atomic rename (System\AtomicFile.mqh). It also stops an
|
|
//--- exclusive writer from blocking that reader's FILE_SHARE_READ|FILE_SHARE_WRITE open on
|
|
//--- another instance.
|
|
int cfgCommonFlag = (common ? FILE_COMMON : 0);
|
|
string cfgTmpName = "";
|
|
int handle = AtomicWriteBegin(configFileName, cfgCommonFlag, cfgTmpName);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print("Error: Unable to open file ", cfgTmpName, " : Error code: ", GetLastError());
|
|
ResetLastError();
|
|
return false;
|
|
}
|
|
//--- ON-DISK LAYOUT - DO NOT REORDER OR RETYPE. Appending a NEW field at the end is the only
|
|
//--- backward-safe change. LoadAndCompareTopologyConfiguration() reads these back positionally,
|
|
//--- and every existing .cfg on every deployed install has this exact sequence; changing it
|
|
//--- silently invalidates them all (-> "configuration mismatch" -> retrain from era 0).
|
|
bool ok = (FileWriteInteger(handle, initialNeuronsCount) >= sizeof(int));
|
|
if(ok && FileWriteInteger(handle, hiddenLayersCount) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteDouble(handle, neuronsReduction) < sizeof(double)) ok = false;
|
|
if(ok && FileWriteInteger(handle, minNeuronsCount) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, optimizationAlgo) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, historyBars) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, outputNeuronsCount) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, neuronsCount) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, studyPeriod) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, minTrainYear) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, isInitialized) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, stopTrainWR) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, fractalPeriods) < sizeof(int)) ok = false;
|
|
//--- APPENDED 2026-07-30, which the note above names as the only backward-safe change. A .cfg
|
|
//--- written before this shipped simply ends here; the loader checks the file length before
|
|
//--- reading them.
|
|
if(ok && FileWriteInteger(handle, convFilterCount) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, lstmHiddenSize) < sizeof(int)) ok = false;
|
|
//--- LEGACY GEOMETRY SLOTS (were SL/TP modes + the derived barrier pair, deleted with the
|
|
//--- barrier stack 2026-08-24). Written as zeros to keep the positional layout: the dirConf
|
|
//--- double and the two pin strings below sit AFTER these four fields in every .cfg on disk.
|
|
if(ok && FileWriteInteger(handle, 0) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteInteger(handle, 0) < sizeof(int)) ok = false;
|
|
if(ok && FileWriteDouble(handle, 0.0) < sizeof(double)) ok = false;
|
|
if(ok && FileWriteDouble(handle, 0.0) < sizeof(double)) ok = false;
|
|
//--- APPENDED 2026-08-09, same append-and-length-guard convention as everything above it.
|
|
if(ok && FileWriteDouble(handle, m_view.BestDirConfThreshold()) < sizeof(double)) ok = false;
|
|
//--- APPENDED 2026-08-11, same append-and-length-guard convention. Written as char-count then
|
|
//--- characters; empty (no set pinned yet) writes 0 and no string, which the reader adopts as
|
|
//--- "nothing pinned" - the state a fresh model is in.
|
|
int xaPinLen = StringLen(m_view.CrossAssetPairsPinned());
|
|
if(ok && FileWriteInteger(handle, xaPinLen) < sizeof(int)) ok = false;
|
|
if(ok && xaPinLen > 0 && FileWriteString(handle, m_view.CrossAssetPairsPinned()) <= 0) ok = false;
|
|
//--- APPENDED 2026-08-16, same append-and-length-guard convention. Width changes already re-key
|
|
//--- the weight fingerprint through neuronsCount; this records which NAMES that width was made
|
|
//--- of.
|
|
int altPinLen = StringLen(m_view.AltDataNamesPinned());
|
|
if(ok && FileWriteInteger(handle, altPinLen) < sizeof(int)) ok = false;
|
|
if(ok && altPinLen > 0 && FileWriteString(handle, m_view.AltDataNamesPinned()) <= 0) ok = false;
|
|
if(!ok)
|
|
{
|
|
Print("Error writing ", configFileName, " : Error code: ", GetLastError());
|
|
ResetLastError();
|
|
}
|
|
return AtomicWriteEnd(handle, configFileName, cfgTmpName, cfgCommonFlag, ok, __FUNCTION__);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::LoadAndCompareTopologyConfiguration(string fileName, int &initialNeuronsCount, int &hiddenLayersCount, double neuronsReduction, int minNeuronsCount, int optimizationAlgo, int &historyBars, int outputNeuronsCount, int neuronsCount, int minTrainYear, bool isInitialized, int stopTrainWR, int fractalPeriods, int &convFilterCount, int &lstmHiddenSize, bool common)
|
|
{
|
|
string configFileName = fileName + ".cfg";
|
|
if(!FileIsExist(configFileName, common ? FILE_COMMON : 0))
|
|
{
|
|
//--- Not an error: no cached config for this parameter set yet - normal on the first run of a config
|
|
//--- (and every first tester/optimizer pass on a fresh agent). The caller treats a false return as
|
|
//--- "start fresh", so log it as informational rather than "Error" (which read as a real failure).
|
|
PrintVerbose(__FUNCTION__ + ": no cached topology config at " + configFileName + " yet - treating as a fresh start for this configuration");
|
|
return false;
|
|
}
|
|
//--- share flags: read-only, see CopySharedFile().
|
|
int handle = FileOpen(configFileName, FILE_READ | FILE_BIN | FILE_SHARE_READ | FILE_SHARE_WRITE | (common ? FILE_COMMON : 0));
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print("Error: Unable to open file ", configFileName);
|
|
return false;
|
|
}
|
|
int savedInitialNeurons = FileReadInteger(handle);
|
|
int savedHiddenLayers = FileReadInteger(handle);
|
|
double savedReductionFactor = FileReadDouble(handle);
|
|
int savedMinNeurons = FileReadInteger(handle);
|
|
int savedOptimizationAlgo = FileReadInteger(handle);
|
|
int savedHistoryBars = FileReadInteger(handle);
|
|
int savedOutputNeuronsCount = FileReadInteger(handle);
|
|
int savedNeuronsCount = FileReadInteger(handle);
|
|
int savedStudyPeriod = FileReadInteger(handle);
|
|
int savedMinTrainYear = FileReadInteger(handle);
|
|
bool savedIsInitialized = FileReadInteger(handle); // read for layout only - NOT compared (see below)
|
|
int savedStopTrainWR = FileReadInteger(handle); // retired MinWR slot - layout only, NOT compared (see below)
|
|
int savedFractalPeriods = FileReadInteger(handle);
|
|
//--- Appended 2026-07-30 - guard on the actual file length rather than reading optimistically, because
|
|
//--- FileReadInteger past the end returns 0 with no error, and adopting a conv filter count of 0 would
|
|
//--- build a degenerate topology out of a file that was merely written by an older build.
|
|
bool haveDerivedStages = (FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(int));
|
|
int savedConvFilters = haveDerivedStages ? FileReadInteger(handle) : 0;
|
|
int savedLstmHidden = haveDerivedStages ? FileReadInteger(handle) : 0;
|
|
//--- LEGACY GEOMETRY SLOTS - read for the positional layout only, never adopted: the barrier
|
|
//--- stack these described was deleted 2026-08-24 and the write side now emits zeros.
|
|
bool haveBarrierGeometry = (FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(int));
|
|
if(haveBarrierGeometry)
|
|
{
|
|
FileReadInteger(handle);
|
|
FileReadInteger(handle);
|
|
}
|
|
bool haveDerivedGeometry = (FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(double));
|
|
if(haveDerivedGeometry)
|
|
{
|
|
FileReadDouble(handle);
|
|
FileReadDouble(handle);
|
|
}
|
|
//--- Appended 2026-08-09. A .cfg from before then simply ends here and the guard yields 0.0, which is
|
|
//--- exactly the right default: unthresholded, i.e. the behaviour that model was trained under.
|
|
bool haveDirConf = (FileSize(handle) >= (ulong)FileTell(handle) + sizeof(double));
|
|
double savedDirConf = haveDirConf ? FileReadDouble(handle) : 0.0;
|
|
//--- Appended 2026-08-11: the cross-asset pair set this model was trained against. Length-guarded
|
|
//--- like everything above; a sanity cap on the count rejects a garbage length from a truncated or
|
|
//--- misaligned file rather than asking FileReadString for megabytes.
|
|
bool haveXaPin = (FileSize(handle) >= (ulong)FileTell(handle) + sizeof(int));
|
|
int xaPinLen = haveXaPin ? FileReadInteger(handle) : 0;
|
|
string savedXaPairs = "";
|
|
if(haveXaPin && xaPinLen > 0 && xaPinLen <= 16 * CROSSASSET_MAX_PAIRS)
|
|
savedXaPairs = FileReadString(handle, xaPinLen);
|
|
//--- Appended 2026-08-16: the alt-data feature name list.
|
|
bool haveAltPin = (FileSize(handle) >= (ulong)FileTell(handle) + sizeof(int));
|
|
int altPinLen = haveAltPin ? FileReadInteger(handle) : 0;
|
|
string savedAltNames = "";
|
|
if(haveAltPin && altPinLen > 0 && altPinLen <= ALTDATA_MAX_PIN_CHARS)
|
|
savedAltNames = FileReadString(handle, altPinLen);
|
|
FileClose(handle);
|
|
//--- Operating point, adopted on the same grounds. Unlike the topology it IS re-fitted every era while
|
|
//--- training continues (the margin distribution moves with the weights), so this restores the value a
|
|
//--- DEPLOYED model should trade at and the value a resuming run starts from until its next pass 2.
|
|
if(haveDirConf && savedDirConf > 0.0)
|
|
{
|
|
m_view.SetDirConfThreshold(savedDirConf);
|
|
m_view.SetBestDirConfThreshold(savedDirConf);
|
|
PrintFormat("%s: adopting the directional confidence threshold this model was trained with - %.2f. "
|
|
"Below that winner-vs-rival softmax margin it abstains rather than trading.",
|
|
__FUNCTION__, savedDirConf);
|
|
}
|
|
//--- Cross-asset pair set: adopt, don't compare, same grounds as the derived barrier - it records
|
|
//--- the panel this model's features were trained against. Already on disk, so the one-shot
|
|
//--- re-save in BuildCrossAssetPanel is moot for this model.
|
|
if(savedXaPairs != "")
|
|
{
|
|
m_view.SetCrossAssetPairsPinned(savedXaPairs);
|
|
m_view.SetCrossAssetCfgSaved(true);
|
|
PrintFormat("%s: adopting the cross-asset pair set this model was trained on - [%s]. The panel "
|
|
"builds from exactly this set; Market Watch changes do not alter it.",
|
|
__FUNCTION__, savedXaPairs);
|
|
}
|
|
//--- Alt-data name list: same adopt-don't-compare grounds. ReadAltDataPinFromCfg() normally
|
|
//--- adopted this before the width sum; a mismatch here means the .cfg changed between the two
|
|
//--- reads (or the pre-reader could not open it) - adopt and say so, because training on inputs
|
|
//--- whose names have silently shifted is exactly the misalignment the pin exists to prevent.
|
|
if(savedAltNames != "" && savedAltNames != m_view.AltDataNamesPinned())
|
|
{
|
|
PrintFormat("%s: alt-data pin from the .cfg [%s] differs from the one applied at width "
|
|
"derivation [%s] - adopting the .cfg's. If the width no longer matches, this run "
|
|
"will (correctly) refuse the saved weights.",
|
|
__FUNCTION__, savedAltNames, m_view.AltDataNamesPinned());
|
|
m_view.SetAltDataNamesPinned(savedAltNames);
|
|
m_view.ApplyAltDataPinnedNames(savedAltNames);
|
|
}
|
|
//--- isInitialized is DELIBERATELY excluded from the comparison: it is a runtime lifecycle flag,
|
|
//--- not a topology/input parameter, and it is always false at the point the compare and the
|
|
//--- fresh-start save run (set true only at the end of InitNeuralNetwork).
|
|
if(savedInitialNeurons > 0)
|
|
initialNeuronsCount = savedInitialNeurons;
|
|
if(savedHiddenLayers > 0)
|
|
hiddenLayersCount = savedHiddenLayers;
|
|
if(savedConvFilters > 0)
|
|
convFilterCount = savedConvFilters;
|
|
if(savedLstmHidden > 0)
|
|
lstmHiddenSize = savedLstmHidden;
|
|
//--- historyBars joined the adopt list 2026-08-11 when the window became DERIVED
|
|
//--- (DeriveHistoryBars): the measurement moves as history downloads, so comparing it would
|
|
//--- discard a trained model for nothing the user did - the exact failure mode this block exists
|
|
//--- to prevent.
|
|
if(savedHistoryBars > 0 && savedHistoryBars != historyBars)
|
|
{
|
|
PrintFormat("%s: adopting the input window this model was trained with - %d bars (a fresh "
|
|
"derivation would have said %d).", __FUNCTION__, savedHistoryBars, historyBars);
|
|
historyBars = savedHistoryBars;
|
|
}
|
|
//--- savedStudyPeriod is read for layout only and NOT compared - the StudyPeriods input it mirrored was
|
|
//--- removed 2026-07-30 (training covers all available history), so like the retired MinWR slot its
|
|
//--- value says nothing about whether the saved WEIGHTS are compatible.
|
|
string diff = "";
|
|
if(savedReductionFactor != neuronsReduction) diff += " reduction " + DoubleToString(savedReductionFactor, 4) + "->" + DoubleToString(neuronsReduction, 4) + ";";
|
|
if(savedMinNeurons != minNeuronsCount) diff += " minNeurons " + IntegerToString(savedMinNeurons) + "->" + IntegerToString(minNeuronsCount) + ";";
|
|
if(savedOptimizationAlgo != optimizationAlgo) diff += " optimizer " + IntegerToString(savedOptimizationAlgo) + "->" + IntegerToString(optimizationAlgo) + ";";
|
|
if(savedOutputNeuronsCount != outputNeuronsCount) diff += " outputs " + IntegerToString(savedOutputNeuronsCount) + "->" + IntegerToString(outputNeuronsCount) + ";";
|
|
if(savedNeuronsCount != neuronsCount) diff += " inputWidth " + IntegerToString(savedNeuronsCount) + "->" + IntegerToString(neuronsCount) + " (a feature toggle changed);";
|
|
if(savedMinTrainYear != minTrainYear) diff += " minTrainYear " + IntegerToString(savedMinTrainYear) + "->" + IntegerToString(minTrainYear) + ";";
|
|
//--- (savedStopTrainWR deliberately NOT compared - retired input, see the note above)
|
|
if(savedFractalPeriods != fractalPeriods) diff += " fractalPeriods " + IntegerToString(savedFractalPeriods) + "->" + IntegerToString(fractalPeriods) + ";";
|
|
if(diff != "")
|
|
{
|
|
Print("Configuration mismatch for ", configFileName, " -> retraining from era 0. Changed:", diff,
|
|
" (a saved model only reloads when these parameters match exactly; revert the changed input to resume the existing model.)");
|
|
FileDelete(configFileName, common ? FILE_COMMON : 0);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Early read of ONE appended .cfg field: the alt-data feature name |
|
|
//| list. |
|
|
//+------------------------------------------------------------------+
|
|
string CModelPersistence::ReadAltDataPinFromCfg(void)
|
|
{
|
|
bool inTesterOrOpt = (bool)MQLInfoInteger(MQL_TESTER) || (bool)MQLInfoInteger(MQL_OPTIMIZATION);
|
|
string fileName = inTesterOrOpt ? (m_view.FileName() + "_optcache") : m_view.FileName();
|
|
int commonFlag = inTesterOrOpt ? 0 : FILE_COMMON;
|
|
if(!FileIsExist(fileName + ".cfg", commonFlag))
|
|
{
|
|
fileName = m_view.FileName();
|
|
commonFlag = FILE_COMMON;
|
|
if(!FileIsExist(fileName + ".cfg", commonFlag))
|
|
return "";
|
|
}
|
|
int handle = FileOpen(fileName + ".cfg", FILE_READ | FILE_BIN | FILE_SHARE_READ | FILE_SHARE_WRITE | commonFlag);
|
|
if(handle == INVALID_HANDLE)
|
|
return "";
|
|
string pin = "";
|
|
//--- fixed layout: 12 ints + 1 double (see SaveTopologyConfiguration's write order)
|
|
bool ok = FileSeek(handle, 12 * sizeof(int) + sizeof(double), SEEK_SET);
|
|
//--- appended segments, in order: conv/lstm ints, sl/tp ints, derived sl/tp doubles,
|
|
//--- dirConf double, xa pin (int + chars), alt pin (int + chars)
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(int))
|
|
ok = FileSeek(handle, 2 * sizeof(int), SEEK_CUR);
|
|
else ok = false;
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(int))
|
|
ok = FileSeek(handle, 2 * sizeof(int), SEEK_CUR);
|
|
else ok = false;
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + 2 * sizeof(double))
|
|
ok = FileSeek(handle, 2 * sizeof(double), SEEK_CUR);
|
|
else ok = false;
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + sizeof(double))
|
|
ok = FileSeek(handle, sizeof(double), SEEK_CUR);
|
|
else ok = false;
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + sizeof(int))
|
|
{
|
|
int xaLen = FileReadInteger(handle);
|
|
if(xaLen > 0 && xaLen <= 16 * CROSSASSET_MAX_PAIRS)
|
|
FileReadString(handle, xaLen); // skip the cross-asset pin (length in CHARS, as written)
|
|
else if(xaLen != 0)
|
|
ok = false; // garbage length: stop walking, yield no pin
|
|
}
|
|
else ok = false;
|
|
if(ok && FileSize(handle) >= (ulong)FileTell(handle) + sizeof(int))
|
|
{
|
|
int altLen = FileReadInteger(handle);
|
|
if(altLen > 0 && altLen <= ALTDATA_MAX_PIN_CHARS)
|
|
pin = FileReadString(handle, altLen);
|
|
}
|
|
FileClose(handle);
|
|
return pin;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| LoadNetWithRetry's operand - see RetryWithBackoff.mqh's |
|
|
//| declaration comment for the shared exponential-backoff shape |
|
|
//| this plugs into (same shape CopyFileWithRetry uses, System\ |
|
|
//| SharedFileCopy.mqh). LoadNetOnce has no quiet param of its own, |
|
|
//| so quiet is simply unused here - nothing to forward it to. |
|
|
//+------------------------------------------------------------------+
|
|
class CLoadNetOnceOp : public IRetryableOp
|
|
{
|
|
private:
|
|
CPersistenceView *m_view; // BORROWED, same as CModelPersistence's own m_view
|
|
public:
|
|
double indicatorParams[];
|
|
CLoadNetOnceOp(CPersistenceView *view) : m_view(view) { }
|
|
virtual bool TryOnce(bool quiet) override { return m_view.LoadNetOnce(indicatorParams); }
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration comment - retries Net.Load() against the |
|
|
//| active file. Covers the same transient-lock class as |
|
|
//| CopyFileWithRetry (e.g. antivirus briefly holding the file just |
|
|
//| written into the tester's local sandbox) rather than assuming any |
|
|
//| single failed read means "no model"/"corrupt file". |
|
|
//+------------------------------------------------------------------+
|
|
bool CModelPersistence::LoadNetWithRetry(double &indicatorParams[])
|
|
{
|
|
const int RETRY_ATTEMPTS = 5;
|
|
const int RETRY_DELAY_CAP_MS = 2000;
|
|
CLoadNetOnceOp op(m_view);
|
|
bool loaded = RetryWithBackoff(GetPointer(op), RETRY_ATTEMPTS, 200, RETRY_DELAY_CAP_MS);
|
|
ArrayCopy(indicatorParams, op.indicatorParams);
|
|
return loaded;
|
|
}
|
|
#endif // WARRIOR_PERSISTENCE_MODELPERSISTENCE_MQH
|
|
//+------------------------------------------------------------------+
|