forked from animatedread/Warrior_EA
Three backends left, as the operator specified: OpenCL, the CPU DLL, and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the tier being removed here; the CPU DLL tier - the one actually used on the training machine (no OpenCL, no DirectML) - is untouched. AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import block and COMPUTE_TIER_GPU (checked first that nothing persists the enum value and only one external site reads .Tier() - safe), collapsed every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call. Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(), member directml/DirectML->computeDll/ComputeDll across every AI/ file that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh. NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code switch and the now-impossible GPU-tier log branch. Verified via per-file brace-balance diff against HEAD and a whole-repo grep for every removed symbol (CDirectMLMy/InitDirectML/ COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional historical-note comment in the new file's header. DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++ source, left in place pending an operator decision. Architecture docs (AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the 4-backend/GPU-tier shape and are not updated in this pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1207 lines
70 KiB
MQL5
1207 lines
70 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| Topology.mqh |
|
|
//| |
|
|
//| Network bootstrap and topology construction: the derived shape |
|
|
//| (width/taper/depth/conv filters/LSTM hidden), the conv, LSTM and |
|
|
//| batch-norm stages, BuildFreshTopology and InitIndicators. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_AIBASE_TOPOLOGY_MQH
|
|
#define WARRIOR_AIBASE_TOPOLOGY_MQH
|
|
//+------------------------------------------------------------------+
|
|
//| THE MODEL FINGERPRINT - every configured value that changes what |
|
|
//| the weights mean, and nothing that does not. Its hash names the |
|
|
//| .nnw/.cfg pair, so this string alone decides when a trained |
|
|
//| model may be resumed and when it must start again from era 0. |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalAIBase::BuildModelFingerprint(void)
|
|
{
|
|
//--- Per-configuration fingerprint appended to the weights filename so that every distinct
|
|
//--- combination of RETRAIN-AFFECTING inputs gets its OWN persistent .nnw/.cfg, instead of all
|
|
//--- combinations sharing one file keyed only on symbol/period/output/optimizer. A model trained
|
|
//--- at 1:3 must never be silently reused at 1:1.
|
|
string fp = StringFormat("%d|%d|%d|%d|%d|%d|%.2f|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d",
|
|
//--- LEGACY_HISTORY_BARS_SLOT: the window left this hash 2026-08-11 when
|
|
//--- it became DERIVED - same rule and reason as every derived field above;
|
|
//--- keyed on a measured quantity, the filename would change the moment more
|
|
//--- history downloads. The .cfg is the record (adopt-don't-compare).
|
|
m_optimizationAlgo, LEGACY_HISTORY_BARS_SLOT, m_outputNeuronsCount,
|
|
m_neuronsCount, m_minTrainYear, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods,
|
|
//--- LEGACY SLOT (was m_focalGamma, removed 2026-07-31). It was a double fed
|
|
//--- to a %d conversion, so it always contributed the literal below rather
|
|
//--- than the configured gamma - the shipped fingerprints read ...|40|0|30|...
|
|
//--- Writing the same literal keeps every existing model's filename intact.
|
|
m_minDirectionalRecallPct, 0, m_oosSplitPct, m_swingConfirmationBars,
|
|
(int)m_useVolumes, (int)m_useTime, (int)m_useATR, (int)m_useSwingContext,
|
|
(int)m_useNews, m_newsFeatureWindowMinutes);
|
|
//--- The one derived value that DOES belong here, and only when it is not derived at all: a
|
|
//--- forced depth is a developer override (see ForceHiddenLayers), so a build that pins one must
|
|
//--- not adopt the .cfg of a build that derived it.
|
|
if(ForceHiddenLayers > 0)
|
|
fp += StringFormat("|FHL:%d", ForceHiddenLayers);
|
|
//--- THE TRIPLE-BARRIER SHAPE LEFT THIS HASH ON 2026-08-07, when SL_Mode/TP_Mode stopped being
|
|
//--- inputs and became MEASURED by ReportBarrierGeometryScan.
|
|
fp += StringFormat("|%d|%d|%d|%d|%d|%d|%d|%d",
|
|
(int)m_useMA, (int)m_useRSI,
|
|
(int)m_useADCumulativeDelta, (int)m_useADShorteningOfThrust,
|
|
(int)m_useADWyckoffEventStream, (int)m_useADWyckoffFailedStructure,
|
|
(int)m_useADWyckoffSignificantBarInversion,
|
|
//--- starting MA TYPE (MA_Type input): changes the MA feature's values, so a change
|
|
//--- must invalidate the cache. The auto-tuned type/period themselves live in the
|
|
//--- .nnw indicator-param block (Flatten/Unflatten), not here - this is the seed only.
|
|
(int)MA_Type);
|
|
//--- MACD/Ichimoku feature flags, appended ONLY WHEN ENABLED rather than unconditionally like
|
|
//--- every flag above. Conditional append leaves those fingerprints byte-identical. Anything
|
|
//--- added here in future should follow the same rule.
|
|
if(m_useMACD)
|
|
fp += StringFormat("|MACD:%d:%d:%d", (int)MACD_PeriodFast, (int)MACD_PeriodSlow, (int)MACD_PeriodSignal);
|
|
if(m_useIchimoku)
|
|
fp += StringFormat("|ICHI:%d:%d:%d", (int)Ichimoku_PeriodTenkan, (int)Ichimoku_PeriodKijun, (int)Ichimoku_PeriodSenkou);
|
|
//--- Cross-asset panel: conditional append, per the rule above, so existing fingerprints are
|
|
//--- untouched. ONLY the flag and the feature count go in. The composition is logged at build
|
|
//--- time and pinned in the .cfg instead.
|
|
if(m_useCrossAsset)
|
|
{
|
|
fp += StringFormat("|XA:%d", CROSSASSET_FEATURES);
|
|
//--- INDEX-MODE RE-ENCODE (2026-08-11). Same width, different SEMANTICS - so models trained
|
|
//--- under the old degenerate encoding must re-key.
|
|
if(SymbolInfoString(m_symbol.Name(), SYMBOL_CURRENCY_BASE) ==
|
|
SymbolInfoString(m_symbol.Name(), SYMBOL_CURRENCY_PROFIT))
|
|
fp += ":IDX2";
|
|
}
|
|
//--- Spread feature: conditional append, same rule. Nothing measured goes in - the spread series
|
|
//--- itself is market data, not configuration.
|
|
if(m_useSpreadFeature)
|
|
fp += "|SPR:2";
|
|
//--- ALT-DATA WINDOW LAYOUT (2026-08-16). The external block now enters the input window ONCE,
|
|
//--- on the newest bar, instead of being replicated on all m_historyBars bars (see
|
|
//--- BuildFeatureWindow for the measurement and the reason).
|
|
if(m_useAltData)
|
|
fp += "|ALTW:2";
|
|
//--- Batch normalization changes the LAYER COUNT, not just the weights, so a model trained with
|
|
//--- it must never load into a topology built without it (and vice versa) - the .cfg guard would
|
|
//--- catch the mismatch and retrain, but only after a confusing failure.
|
|
if(EnableBatchNorm && BatchNormWindow > 1)
|
|
fp += StringFormat("|BN:%d", BatchNormWindow);
|
|
//--- Changes the training gradient, so a model trained with it must never load into a run
|
|
//--- without it. ":BS" = the correction spans Buy/Sell only, with Neutral (the abstain outcome)
|
|
//--- never subsidised - see ApplyLogitAdjustment.
|
|
if(m_logitAdjustTau > 0.0)
|
|
fp += StringFormat("|LA:%d:BS", (int)MathRound(m_logitAdjustTau * 100.0));
|
|
//--- 2026-07-29 audit of every input in Variables\Inputs.mqh against this hash. LEGACY SLOT.
|
|
//--- Same treatment as LEGACY_CONVERGE_WR_SLOT / LEGACY_STUDY_PERIOD_SLOT.
|
|
fp += "|MR:1:90:1";
|
|
//--- Feature-value inputs, each conditional on the feature that reads it actually being on - the
|
|
//--- same rule the MACD/Ichimoku blocks above follow. All three also feed the CLASSIC MA/RSI
|
|
//--- votes, which are inference-only; gating on the AI feature flag is what keeps a classic-
|
|
//--- signal tweak from re-keying a model that never saw it.
|
|
if(m_useVolumes)
|
|
fp += StringFormat("|VOL:%d", (int)VolumeData);
|
|
if(m_useMA)
|
|
fp += StringFormat("|MAP:%d", (int)PeriodMA);
|
|
if(m_useRSI)
|
|
fp += StringFormat("|RSIP:%d", (int)PeriodRSI);
|
|
//--- AD/WYCKOFF PARAMETERS. That rule is not theoretical here: the 2026-07-29 audit found five
|
|
//--- inputs changing trained weights without changing the filename, which is the exact trap the
|
|
//--- .nnw architecture incident already cost a day to.
|
|
fp += "|WIN:2";
|
|
//--- WYCKOFF CATEGORICAL ENCODING VERSION. Bump the number rather than adding a flag if the
|
|
//--- encoding is ever revisited.
|
|
if(m_useADWyckoffEventStream)
|
|
fp += "|WES:2";
|
|
//--- TRAINING TARGET (meta-labeling). Conditional, so every existing direction model keeps its
|
|
//--- byte-identical fingerprint. The 2-output head and the State\META\ folder already separate
|
|
//--- the FILES; this separates the SEMANTICS.
|
|
if(IsMetaTarget())
|
|
fp += "|TGT:META1";
|
|
//--- Same contract for the fractal-direction target (TrainingTarget input): the token covers the
|
|
//--- label's meaning (next confirmed 5-bar fractal extreme, min-move floor, tie handling) - bump
|
|
//--- the number if any of that changes.
|
|
if(IsFractalTarget())
|
|
fp += "|TGT:FRA1";
|
|
//--- Ensemble membership separates the FILES, not the semantics: the member's topology and label
|
|
//--- are identical to its solo twin, but the two must never share weights across charts (the
|
|
//--- duplicate-chart guard exists precisely to stop concurrent writers).
|
|
if(m_ensembleMember)
|
|
fp += "|ENS1";
|
|
//--- The scheduled close-all became part of the LABEL'S MEANING on 2026-08-19:
|
|
//--- TripleBarrierLabel stops its walk at the next scheduled flat, so the same chart with a
|
|
//--- Friday-23:45 schedule and with an everyday-22:00 schedule trains two DIFFERENT targets.
|
|
if((int)targetDayOfWeek != -1 && (int)targetHour != -1 && (int)targetMinutes != -1)
|
|
fp += "|CUT:" + IntegerToString((int)targetDayOfWeek) + "@" + IntegerToString((int)targetHour) +
|
|
":" + IntegerToString((int)targetMinutes);
|
|
return fp;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Common network bootstrap shared by every AI signal: sets up |
|
|
//| indicators, then loads a saved network or builds a fresh one |
|
|
//| whose only per-signal-type difference is AddCustomLayers(). |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitNeuralNetwork(CIndicators *indicators)
|
|
{
|
|
if(m_isInitialized)
|
|
return true;
|
|
if(indicators == NULL)
|
|
return false;
|
|
m_indicatorsPtr = indicators;
|
|
if(!CExpertSignalCustom::InitIndicators(indicators))
|
|
return false;
|
|
if(!InitFeatureIndicators(indicators))
|
|
return false;
|
|
//--- Kick the terminal's async history sync for every cross-asset reference symbol NOW, at init,
|
|
//--- so the ~minute of cross-symbol download runs while the model loads and the label cache
|
|
//--- prebuilds - instead of starting only when the first Build() call finds the symbols unselected
|
|
//--- and the first era (and the one-shot MI report) runs with the panel absent. Non-blocking.
|
|
if(m_useCrossAsset)
|
|
m_crossAsset.Warm((ENUM_TIMEFRAMES)m_period);
|
|
Net = new CNet(NULL);
|
|
if(CheckPointer(Net) == POINTER_INVALID)
|
|
return false;
|
|
//--- Size the first dense layer to the data. InitIndicators() above is what finalises
|
|
//--- m_neuronsCount, so this is the earliest point the input width is actually known. ORDER
|
|
//--- MATTERS, and it changed on 2026-08-09.
|
|
m_historyBars = DeriveHistoryBars();
|
|
m_convFilterCount = ComputeConvFilterCount();
|
|
m_lstmHiddenSize = ComputeLstmHiddenSize();
|
|
m_initialNeuronsCount = ComputeFirstLayerWidth();
|
|
//--- Depth LAST of the four: it is derived from the first-layer width above, so it cannot be settled
|
|
//--- before that one is. All four are overwritten from the .cfg further below if this configuration
|
|
//--- already has a trained model - see the adopt-don't-compare block there.
|
|
m_hiddenLayersCount = ComputeHiddenLayerCount();
|
|
//--- The name used to carry a dense-depth tag ("Perceptron 3L"), from when AIType let a user
|
|
//--- pick MLP_3L vs MLP_4L and the depth was the only thing separating two charts of the same
|
|
//--- family.
|
|
string fp = BuildModelFingerprint();
|
|
//--- FNV-1a 32-bit -> 8 hex chars: compact, deterministic, order-stable, collision-safe enough for
|
|
//--- the small optimizer grids in play (a collision would merely fail the .cfg guard and retrain).
|
|
uint fpHash = 2166136261;
|
|
int fpLen = StringLen(fp);
|
|
for(int fpi = 0; fpi < fpLen; fpi++)
|
|
{
|
|
fpHash ^= (uint)StringGetCharacter(fp, fpi);
|
|
fpHash *= 16777619;
|
|
}
|
|
m_fileName += "_" + DoubleToString(MathRound(m_outputNeuronsCount)) + "_" + DoubleToString(MathRound(m_optimizationAlgo)) + "_" + StringFormat("%08x", fpHash);
|
|
//--- Finish the display name with the model's short id and the leading 4 hex digits of that same
|
|
//--- fingerprint, so every log line and panel names the model file it belongs to. Their files
|
|
//--- were never at risk; the TAG was simply unable to do its one job.
|
|
string cfgTag = " [" + m_id + "-" + StringSubstr(StringFormat("%08x", fpHash), 0, 4) + "]";
|
|
if(StringFind(ID, cfgTag) < 0)
|
|
ID += cfgTag;
|
|
//--- One self-verifying config line per chart, deliberately NOT gated on VerboseMode. A multi-
|
|
//--- chart comparison is only valid if every chart is identical except the axis under test, and
|
|
//--- until now a drifted setting was invisible: the filename carries a HASH, so two charts that
|
|
//--- should match and do not look merely "different" with no indication of WHICH field moved.
|
|
Print(ID + ": config - " + IntegerToString(m_hiddenLayersCount) + " dense from " +
|
|
IntegerToString(m_initialNeuronsCount) + " units | batchnorm " +
|
|
((EnableBatchNorm && BatchNormWindow > 1) ? "ON(" + IntegerToString(BatchNormWindow) + ")" : "OFF") +
|
|
//--- "requested", not the bare number: the EFFECTIVE tau is capped against the head's
|
|
//--- usable logit range and cannot be known until the class priors are measured, so
|
|
//--- printing 1.00 here read as the value in force when every chart was actually running
|
|
//--- 0.35.
|
|
" | class-imbalance " + (m_logitAdjustTau > 0.0
|
|
? "logit-adjust(tau " + DoubleToString(m_logitAdjustTau, 2) + " requested)" : "OFF") +
|
|
" | input " + IntegerToString((int)m_historyBars * m_neuronsCount) +
|
|
" (" + IntegerToString((int)m_historyBars) + " bars x " + IntegerToString(m_neuronsCount) + ")" +
|
|
//--- The front-end stages are DERIVED (see ComputeConvFilterCount/ComputeLstmHiddenSize),
|
|
//--- so without them this "self-verifying" line verified only half the topology - it
|
|
//--- printed the dense taper while the conv/recurrent stages that actually dominate
|
|
//--- CONV/LSTM/HYBRID were invisible.
|
|
FrontEndConfigSummary());
|
|
//--- Kept as its own line and deliberately free of any per-chart prefix INSIDE the string, so
|
|
//--- the six startup lines diff textually against each other.
|
|
Print(ID + ": fingerprint - " + fp);
|
|
//--- The resolved path is DebuggingMode-only: the tag above already names the folder (its m_id half)
|
|
//--- and the file's hash suffix (its hex half), so this line is derivable rather than new information,
|
|
//--- and a third startup line per chart is not worth spending on a user who will never open the file.
|
|
if(DebuggingMode)
|
|
Print(ID + ": model file - " + m_fileName + ".nnw");
|
|
//--- Strategy Tester / optimizer: target a LOCAL (agent-sandboxed, non-FILE_COMMON) cache file
|
|
//--- instead of the shared production weights, so genetic/complete optimization passes on this
|
|
//--- same agent can reuse an already-trained model whenever the topology-relevant inputs
|
|
//--- (neuron counts, layers, history bars, output count, opt algo, study period, ...) are
|
|
//--- unchanged from a previous pass, instead of re-running every training era from scratch each
|
|
//--- pass. The live/manual-chart production .nnw/.cfg under FILE_COMMON are never touched by
|
|
//--- this path, so a backtest can never corrupt or overwrite the deployed live model.
|
|
bool inTesterOrOpt = MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD);
|
|
m_activeFileName = inTesterOrOpt ? (m_fileName + "_optcache") : m_fileName;
|
|
m_activeFileCommon = !inTesterOrOpt;
|
|
//--- Claim these files before anything reads or writes them, and refuse to start if another
|
|
//--- chart in this terminal already holds them (see AcquireConfigLock). SKIPPED under
|
|
//--- m_exportFeaturesOnly: that mode reads history and writes one CSV - it never trains, never
|
|
//--- saves a model (Warrior_EA.mq5's OnTick returns immediately, so no era ever completes) and
|
|
//--- therefore has nothing to protect against a concurrent chart.
|
|
if(!m_exportFeaturesOnly && !inTesterOrOpt && !AcquireConfigLock())
|
|
return false;
|
|
//--- Any Strategy-Tester run - a single backtest OR an optimization pass - runs pure inference
|
|
//--- on the deployed model, never trains.
|
|
m_inferenceOnly = MQLInfoInteger(MQL_TESTER);
|
|
//--- Seed the agent-local optcache from the deployed production model on the first tester/opt
|
|
//--- pass. Re-seeds when the cache is MISSING *or* STALE. Copies FROM FILE_COMMON (the
|
|
//--- live/manual-chart model) INTO the agent-local sandbox only; the production files are read,
|
|
//--- never written, so a backtest still can't corrupt the deployed model.
|
|
bool cacheMissing = !FileIsExist(m_activeFileName + ".nnw");
|
|
bool cacheStale = false;
|
|
if(inTesterOrOpt && !cacheMissing && FileIsExist(m_fileName + ".nnw", FILE_COMMON))
|
|
{
|
|
datetime prodModified = (datetime)FileGetInteger(m_fileName + ".nnw", FILE_MODIFY_DATE, true);
|
|
datetime cacheModified = (datetime)FileGetInteger(m_activeFileName + ".nnw", FILE_MODIFY_DATE, false);
|
|
//--- both timestamps must be readable before trusting the comparison; a 0 means "couldn't tell",
|
|
//--- and re-seeding on an unreadable timestamp every single pass would be worse than not checking.
|
|
cacheStale = (prodModified > 0 && cacheModified > 0 && prodModified > cacheModified);
|
|
if(cacheStale)
|
|
Print(__FUNCTION__ + ": the deployed model is newer than this agent's cached copy - re-seeding so the backtest runs the CURRENT model, not the previously cached one.");
|
|
}
|
|
if(inTesterOrOpt && (cacheMissing || cacheStale))
|
|
{
|
|
if(FileIsExist(m_fileName + ".nnw", FILE_COMMON))
|
|
{
|
|
//--- The .nnw is the only copy that MUST succeed - retried (see CopyFileWithRetry's
|
|
//--- declaration comment) because a live chart's own atomic Save() can be mid-rename on
|
|
//--- this exact file.
|
|
if(CopyFileWithRetry(m_fileName + ".nnw", m_activeFileName + ".nnw"))
|
|
{
|
|
//--- Best-effort sidecars: not retried - losing one just means a cold
|
|
//--- calibration/shadow-blend start rather than a wrong/untrained model, which the .nnw
|
|
//--- copy above already guards against.
|
|
if(FileIsExist(m_fileName + ".cfg", FILE_COMMON))
|
|
CopySharedFile(m_fileName + ".cfg", m_activeFileName + ".cfg", false);
|
|
if(FileIsExist(m_fileName + "_shadow.nnw", FILE_COMMON))
|
|
CopySharedFile(m_fileName + "_shadow.nnw", m_activeFileName + "_shadow.nnw", false);
|
|
//--- carry the calibration sidecar into the agent sandbox too, so a seeded backtest calibrates its
|
|
//--- live decisions with the deployed model's priors instead of the un-adjusted cold defaults.
|
|
if(FileIsExist(m_fileName + ".stats", FILE_COMMON))
|
|
CopySharedFile(m_fileName + ".stats", m_activeFileName + ".stats", false);
|
|
Print(__FUNCTION__ + ": seeded tester cache from the deployed production model (" + m_fileName + ") - this run reuses the deployed weights instead of retraining");
|
|
}
|
|
//--- else: CopyFileWithRetry already logged why. Fall through - the Net.Load() below will
|
|
//--- correctly report "no file" and BuildFreshTopology() takes over, same as a genuine first pass.
|
|
}
|
|
else if(m_inferenceOnly)
|
|
//--- Name the exact file (symbol + timeframe + config fingerprint) it looked for: the
|
|
//--- model is keyed on the CHART TIMEFRAME, so the #1 cause of this is running the tester
|
|
//--- on a different timeframe than the model was trained on (e.g. an H4 model, tester set
|
|
//--- to H1) - which reads as "no model" when one exists under a different timeframe.
|
|
Print(__FUNCTION__ + ": WARNING - no deployed production model found at '" + m_fileName +
|
|
".nnw' (shared folder) for " + _Symbol + " " + EnumToString((ENUM_TIMEFRAMES)_Period) +
|
|
". A single backtest runs inference only and will NOT train. Most common cause: the tester" +
|
|
" timeframe differs from the one the model was trained on (the filename is keyed on timeframe)." +
|
|
" Otherwise, train this configuration on a chart first, then re-run the backtest.");
|
|
}
|
|
if(!LoadAndCompareTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount, m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo, m_historyBars, m_outputNeuronsCount, m_neuronsCount, m_minTrainYear, m_isInitialized, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount, m_lstmHiddenSize, m_activeFileCommon))
|
|
{
|
|
//--- Topology/input params diverged from what produced the saved .nnw (or no .cfg exists yet;
|
|
//--- for inTesterOrOpt this is also the normal "first pass on this agent" case).
|
|
if(FileIsExist(m_activeFileName + ".nnw", m_activeFileCommon ? FILE_COMMON : 0))
|
|
{
|
|
Print(__FUNCTION__ + ": " + m_activeFileName + " - topology/input params changed since last save; discarding incompatible saved weights and starting fresh");
|
|
FileDelete(m_activeFileName + ".nnw", m_activeFileCommon ? FILE_COMMON : 0);
|
|
//--- Reaching here means a TRAINED model was just thrown away, so its drawn signals are
|
|
//--- stale for exactly the same reason ResetWeights() clears them: they would otherwise be
|
|
//--- restored moments later (LoadChartSignals runs at the end of this function) and shown
|
|
//--- as if they belonged to the model about to be trained.
|
|
ClearPersistedChartSignals("saved weights discarded - topology/input params changed");
|
|
}
|
|
if(FileIsExist(m_activeFileName + "_ckpt.tmp", m_activeFileCommon ? FILE_COMMON : 0))
|
|
FileDelete(m_activeFileName + "_ckpt.tmp", m_activeFileCommon ? FILE_COMMON : 0);
|
|
// Same reasoning applies to the EMA shadow-weight file (see m_shadowNet's declaration comment) -
|
|
// it's shaped for the OLD topology too, and EnsureShadowNet() has no independent way to detect
|
|
// that mismatch on Load() (CNet::Load() doesn't cross-validate against an expected shape). Drop
|
|
// it so EnsureShadowNet() cleanly misses and re-bootstraps from the fresh Net instead.
|
|
if(FileIsExist(m_activeFileName + "_shadow.nnw", m_activeFileCommon ? FILE_COMMON : 0))
|
|
FileDelete(m_activeFileName + "_shadow.nnw", m_activeFileCommon ? FILE_COMMON : 0);
|
|
//--- the calibration sidecar is tied to the discarded weights - drop it too so a fresh run
|
|
//--- re-measures priors from scratch instead of adjusting with a stale model's base rates.
|
|
if(FileIsExist(m_activeFileName + ".stats", m_activeFileCommon ? FILE_COMMON : 0))
|
|
FileDelete(m_activeFileName + ".stats", m_activeFileCommon ? FILE_COMMON : 0);
|
|
//--- and the pattern-database backfill marker (see StartPatternDatabaseBackfill): it records
|
|
//--- the era of the model whose OOS calls were written into the ranking tables.
|
|
if(FileIsExist(m_activeFileName + ".dbfill", m_activeFileCommon ? FILE_COMMON : 0))
|
|
FileDelete(m_activeFileName + ".dbfill", m_activeFileCommon ? FILE_COMMON : 0);
|
|
SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount, m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo, m_historyBars, m_outputNeuronsCount, m_neuronsCount, LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, m_isInitialized, LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount, m_lstmHiddenSize, m_activeFileCommon);
|
|
}
|
|
double loadedIndicatorParams[];
|
|
//--- Inference-only backtest: if this deployed model was validated MQL5-inference-safe at deploy
|
|
//--- (marker in its .stats), load it host-only and run the pure-MQL5 forward path so the backtest
|
|
//--- never loads WarriorDML/WarriorCPU.dll - no DLL file-lock class of failure, and the exact math
|
|
//--- the Market build ships. Falls back to a compute backend just below if that load fails.
|
|
if(m_inferenceOnly && CheckPointer(Net) != POINTER_INVALID)
|
|
{
|
|
LoadModelStats(m_activeFileName, m_activeFileCommon); // reads m_mqlInferenceValidated (and priors)
|
|
if(m_mqlInferenceValidated)
|
|
{
|
|
Net.SetCpuInference(true);
|
|
PrintVerbose(__FUNCTION__ + ": " + ID + " - inference-only backtest running pure-MQL5 (DLL-free): the deployed model is validated MQL5-inference-safe");
|
|
}
|
|
}
|
|
bool netLoaded = LoadNetWithRetry(loadedIndicatorParams);
|
|
//--- Pure-MQL5 load failed unexpectedly (should not happen for a validated model) - drop back to a
|
|
//--- compute backend and retry once so the backtest still runs via the DLL rather than on a fresh net.
|
|
if(!netLoaded && CheckPointer(Net) != POINTER_INVALID && Net.CpuInference())
|
|
{
|
|
Print(__FUNCTION__ + ": " + ID + " - pure-MQL5 load failed; retrying with a compute backend (DLL)");
|
|
Net.SetCpuInference(false);
|
|
netLoaded = LoadNetWithRetry(loadedIndicatorParams);
|
|
}
|
|
//--- the file may carry a superseded architecture - correct it before anything reads the net
|
|
if(netLoaded)
|
|
EnforceTopologyContract();
|
|
//--- A superseded conv receptive field cannot be repaired in place (different weight-tensor shape),
|
|
//--- so the loaded net is discarded and the fresh-topology path below rebuilds and retrains.
|
|
if(netLoaded && m_topologySuperseded)
|
|
netLoaded = false;
|
|
//--- restore the calibration sidecar (priors + confidence scale) that pairs with these weights, so a
|
|
//--- restart - including a buyer's inference-only backtest - calibrates live decisions exactly as the
|
|
//--- saved model did instead of running with cold defaults (priors 0 => no adjustment). See LoadModelStats().
|
|
if(netLoaded)
|
|
LoadModelStats(m_activeFileName, m_activeFileCommon);
|
|
m_modelLoadedFromDisk = netLoaded;
|
|
//--- Make a successful resume visible (the counterpart to the fresh-start / mismatch messages below):
|
|
//--- on a live chart this confirms the saved model was found and loaded rather than silently retrained.
|
|
if(netLoaded && !inTesterOrOpt)
|
|
Print(ID + ": resumed saved model from era " + IntegerToString(m_eraCount) + " (trainingComplete=" + (string)m_trainingComplete + ") - continuing, not retraining from era 0.");
|
|
//--- RESUMED MODELS GET THE SAME WARM-UP AS FRESH ONES (2026-08-13; was `netLoaded ? 0 : 3`).
|
|
//--- Three no-op passes cost seconds. The label cache itself, however, is NEVER restored from
|
|
//--- the .nnw checkpoint - it lives only in the in-memory m_labelCacheBuy/Sell/HasValue arrays,
|
|
//--- which start empty every process start regardless of netLoaded.
|
|
m_warmupPassesRemaining = 3;
|
|
m_labelCachePrebuilt = false;
|
|
if(inTesterOrOpt && netLoaded)
|
|
Print(__FUNCTION__ + ": " + ID + " - reused cached weights from a previous optimization/tester pass on this agent (era " + IntegerToString(m_eraCount) + ", trainingComplete=" + (string)m_trainingComplete + ") - skipping redundant training for this unchanged config");
|
|
if(netLoaded && ArraySize(loadedIndicatorParams) == AD_TUNE_PARAM_COUNT)
|
|
{
|
|
//--- Restart deploying previously AutoTune-d indicator params even with
|
|
//--- AutoTuneIndicators=false now.
|
|
AdoptIndicatorParams(loadedIndicatorParams, indicators);
|
|
}
|
|
if(!netLoaded)
|
|
{
|
|
int error_code = GetLastError();
|
|
//--- Do NOT present error_code as the cause: on a no-GPU/CPU-DLL box it is the harmless 5100
|
|
//--- (OpenCL-not-found) left by the compute probe inside CNet::Load, NOT the reason the file
|
|
//--- was rejected.
|
|
if(error_code != 5004) // not "file not found"
|
|
ResetLastError();
|
|
//--- CRITICAL: a failed load may have ALREADY overwritten the training-state out-params from
|
|
//--- the bad file's header before it was rejected - notably a corrupt/empty 0-layer stub
|
|
//--- whose header still says trainingComplete=1 (see CNet::Load's 0-layer guard).
|
|
m_trainingComplete = false;
|
|
m_eraCount = 0;
|
|
dtStudied = 0;
|
|
dForecast = 0;
|
|
//--- Cold the in-memory calibration so the freshly-rebuilt (untrained) topology below runs
|
|
//--- with no stale prior-correction until a retrain re-measures it (priors 0 =>
|
|
//--- AdjustedSignalFromSoftmax is a no-op; scale 1.0 = the constructor default).
|
|
m_priorBuy = 0.0;
|
|
m_priorSell = 0.0;
|
|
m_priorNeutral = 0.0;
|
|
m_confidenceCalScale = 1.0;
|
|
//--- Accurate diagnostic (do NOT cite GetLastError() - inside CNet::Load the OpenCL probe leaves 5100
|
|
//--- there on a no-GPU/CPU-DLL box, which has nothing to do with the file). Distinguish an ordinary
|
|
//--- fresh start (no file yet) from a real read failure of an existing file by testing existence.
|
|
if(!inTesterOrOpt)
|
|
{
|
|
int loadFlags = m_activeFileCommon ? FILE_COMMON : 0;
|
|
if(FileIsExist(m_activeFileName + ".nnw", loadFlags))
|
|
Print(ID + ": could not read the existing model file " + m_activeFileName + ".nnw - rebuilding a fresh topology to retrain from era 0. Existing .stats/_shadow.nnw are KEPT (they refresh as training runs). If this recurs, that .nnw is likely corrupt - back it up, then use the panel's reset-weights to start clean.");
|
|
else
|
|
Print(ID + ": no saved model for this config yet - starting a fresh training run from era 0.");
|
|
}
|
|
//--- Re-seed before building a fresh topology so weight init is genuinely random. See
|
|
//--- System\Random.mqh. Matches ResetWeights() and OnInit.
|
|
WarriorRandSeed(ID);
|
|
//--- Era 0 with no weights behind it, so any arrow currently on this chart was drawn by a
|
|
//--- DIFFERENT model - the previous fingerprint's, or a corrupt .nnw's. Deliberately at
|
|
//--- this call site rather than inside BuildFreshTopology(): the genetic tuner calls that
|
|
//--- for every throwaway candidate (AutoTune.mqh) and must not touch the chart.
|
|
ClearPersistedChartSignals("fresh topology at era 0 - arrows belong to a previous model");
|
|
if(!BuildFreshTopology())
|
|
return false;
|
|
}
|
|
TempData = new CArrayDouble();
|
|
if(CheckPointer(TempData) == POINTER_INVALID)
|
|
return false;
|
|
if(netLoaded)
|
|
// Populate dPrevSignal from the just-loaded weights immediately, rather than leaving it at
|
|
// its blank constructor default until the next (asynchronous, queued) training pass happens
|
|
// to run - matters most for the tester cache-reuse path above, where training may be skipped
|
|
// entirely for this run because dtStudied already covers the whole backtest window.
|
|
RefreshLatestSignal();
|
|
//--- Status line must match what the gate below (if(!m_trainingComplete && !m_inferenceOnly)) will
|
|
//--- actually do - otherwise an inference-only single backtest logs "resuming full training now" right
|
|
//--- under the "runs inference only and will NOT train" warning, which reads as a contradiction.
|
|
string trainState = m_trainingComplete
|
|
? "already complete - staying converged, no full retrain on this restart"
|
|
: (m_inferenceOnly
|
|
? "NOT complete, but this is an inference-only backtest - NOT training (see warning above); deploy a trained model for meaningful results"
|
|
: "NOT complete (interrupted or never converged) - resuming full training now");
|
|
Print(__FUNCTION__ + ": " + m_activeFileName + " - training " + trainState);
|
|
//--- Only kick off a full Train() run here if the loaded model genuinely isn't converged yet - an
|
|
//--- already-complete model used to get one full era-loop retrain (real Net.backProp() over the
|
|
//--- whole IS window) on every single EA restart/reattach for no reason, since this "Init" event
|
|
//--- bypassed ScheduleTrainingIfNeeded()'s m_trainingComplete gate entirely. dPrevSignal is already
|
|
//--- fresh from RefreshLatestSignal() above; ScheduleTrainingIfNeeded()'s normal per-tick check
|
|
//--- will call RefreshConvergedSignal() itself once a genuinely new bar closes.
|
|
if(!m_trainingComplete && !m_inferenceOnly)
|
|
ArmStudyEvent((long)MathMax(0, MathMin(iTime(_Symbol, PERIOD_CURRENT, (int)(100 * Net.recentAverageSmoothingFactor * (m_trainingComplete ? 1 : 10))), dtStudied)), "Init");
|
|
//--- Restore arrows persisted from a previous session (see SaveChartSignals). MUST run here, not in
|
|
//--- InitIndicators(): the arrows file is keyed on the FULL m_fileName including the per-config
|
|
//--- fingerprint, which is only appended above - see the note left at InitIndicators()'s old call site.
|
|
LoadChartSignals();
|
|
//--- bootstrap (or restore) the EMA shadow net now rather than waiting for the first
|
|
//--- RefreshLatestSignal()/era-blend call to lazily trigger it - see m_shadowNet's declaration
|
|
//--- comment.
|
|
EnsureShadowNet();
|
|
m_isInitialized = true;
|
|
//--- RESEARCH ONLY, under m_exportFeaturesOnly. Runs here because this is the first point at which
|
|
//--- the indicators, the buffers and the derived barrier horizon are all settled, and it needs no
|
|
//--- model, no labels and no training.
|
|
if(m_exportFeaturesOnly)
|
|
ExportFeatureMatrix();
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Shared training-set size estimate - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalAIBase::EstimatedInSampleBarsRaw(void) const
|
|
{
|
|
int secs = PeriodSeconds(m_period);
|
|
if(secs <= 0)
|
|
secs = PeriodSeconds(PERIOD_H1);
|
|
double barsPerYear = (SECONDS_PER_YEAR / (double)secs) * MARKET_OPEN_FRACTION;
|
|
double oosKept = (100.0 - (double)m_oosSplitPct) / 100.0;
|
|
//--- MEASURED from the symbol's real history, matching Train()'s window exactly (earliest
|
|
//--- available bar, floored by MinTrainYear) now that training covers everything available
|
|
//--- rather than a configured number of years.
|
|
datetime firstAvailableBar = (datetime)SeriesInfoInteger(_Symbol, m_period, SERIES_FIRSTDATE);
|
|
MqlDateTime floorTime;
|
|
TimeCurrent(floorTime);
|
|
floorTime.year = m_minTrainYear;
|
|
floorTime.mon = 1;
|
|
floorTime.day = 1;
|
|
floorTime.hour = 0;
|
|
floorTime.min = 0;
|
|
floorTime.sec = 0;
|
|
datetime windowStart = StructToTime(floorTime);
|
|
if(firstAvailableBar > windowStart)
|
|
windowStart = firstAvailableBar;
|
|
int available = Bars(_Symbol, m_period, windowStart, TimeCurrent());
|
|
//--- History may not have finished syncing when a chart first attaches, and a model whose
|
|
//--- capacity was pinned from a handful of bars would stay crippled for its whole life - the one
|
|
//--- failure mode that measuring instead of assuming introduces.
|
|
if(available < TOPOLOGY_BUDGET_MIN_TRUSTED_BARS)
|
|
{
|
|
Print(ID + ": WARNING - only " + IntegerToString(available) + " bars of " + _Symbol +
|
|
" history are available yet, too few to size the network from. Falling back to a " +
|
|
IntegerToString(TOPOLOGY_BUDGET_FALLBACK_YEARS) + "-year assumption. If this is a fresh" +
|
|
" install, let the terminal finish downloading history and then delete this model's weights" +
|
|
" from the panel so the topology is sized from the real data.");
|
|
return (double)TOPOLOGY_BUDGET_FALLBACK_YEARS * barsPerYear * oosKept;
|
|
}
|
|
return (double)available * oosKept;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| In-sample budget in INDEPENDENT observations - see declaration. |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalAIBase::EstimatedInSampleBars(void) const
|
|
{
|
|
//--- DEFLATED BY LABEL OVERLAP (2026-08-19). 4 - the same correction every standard error in
|
|
//--- this file already applies).
|
|
return EffectiveSampleSize(EstimatedInSampleBarsRaw());
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Fan-in of the first dense layer - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::FirstLayerFanIn(void) const
|
|
{
|
|
int frontEndOut = UsesLstmStage() ? m_lstmHiddenSize
|
|
: (UsesConvStage() ? ConvOutputWidth() : 0);
|
|
return (frontEndOut > 0) ? frontEndOut : (int)m_historyBars * m_neuronsCount;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Derived input-window length - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::DeriveHistoryBars(void)
|
|
{
|
|
int availableBars = Bars(_Symbol, m_period);
|
|
int span = (int)MathMin(availableBars - 1, WINDOW_DERIVE_SPAN_BARS);
|
|
if(span < TOPOLOGY_BUDGET_MIN_TRUSTED_BARS)
|
|
{
|
|
Print(ID + ": WARNING - only " + IntegerToString(availableBars) + " bars of " + _Symbol +
|
|
" history are available yet, too few to measure the input window from. Falling back to " +
|
|
IntegerToString(HISTORY_BARS_FALLBACK) + " bars. If this is a fresh install, let history "
|
|
"finish downloading and delete this model's weights so the window is measured from real data.");
|
|
return HISTORY_BARS_FALLBACK;
|
|
}
|
|
//--- newest CLOSED bars only (start 1): the forming bar's extremes are still moving
|
|
double hi[], lo[];
|
|
ArraySetAsSeries(hi, true);
|
|
ArraySetAsSeries(lo, true);
|
|
if(CopyHigh(_Symbol, m_period, 1, span, hi) != span ||
|
|
CopyLow(_Symbol, m_period, 1, span, lo) != span)
|
|
{
|
|
Print(ID + ": WARNING - could not read " + IntegerToString(span) + " bars to measure the input "
|
|
"window; falling back to " + IntegerToString(HISTORY_BARS_FALLBACK) + " bars.");
|
|
return HISTORY_BARS_FALLBACK;
|
|
}
|
|
//--- Swing pivots: strict local extremum against the NEWER side, >= against the older side (the
|
|
//--- standard tie-break so a flat top counts once).
|
|
double legs[];
|
|
ArrayResize(legs, 0, 256);
|
|
int lastType = 0; // +1 swing high, -1 swing low, 0 none yet
|
|
int lastPivotBar = -1;
|
|
double lastExtreme = 0.0;
|
|
for(int b = span - 1 - WINDOW_SWING_WING; b >= WINDOW_SWING_WING; b--) // oldest -> newest
|
|
{
|
|
bool isHigh = true, isLow = true;
|
|
for(int w = 1; w <= WINDOW_SWING_WING && (isHigh || isLow); w++)
|
|
{
|
|
if(hi[b] <= hi[b - w] || hi[b] < hi[b + w])
|
|
isHigh = false;
|
|
if(lo[b] >= lo[b - w] || lo[b] > lo[b + w])
|
|
isLow = false;
|
|
}
|
|
int type = 0;
|
|
if(isHigh != isLow)
|
|
type = isHigh ? 1 : -1; // a bar that is both is degenerate; skip it
|
|
if(type == 0)
|
|
continue;
|
|
if(type == lastType)
|
|
{
|
|
double x = (type == 1) ? hi[b] : lo[b];
|
|
if((type == 1 && x > lastExtreme) || (type == -1 && x < lastExtreme))
|
|
{
|
|
lastPivotBar = b;
|
|
lastExtreme = x;
|
|
}
|
|
continue;
|
|
}
|
|
if(lastType != 0)
|
|
{
|
|
int n = ArraySize(legs);
|
|
ArrayResize(legs, n + 1, 256);
|
|
legs[n] = lastPivotBar - b; // series indices: newer bar = smaller index
|
|
}
|
|
lastType = type;
|
|
lastPivotBar = b;
|
|
lastExtreme = (type == 1) ? hi[b] : lo[b];
|
|
}
|
|
if(ArraySize(legs) < WINDOW_DERIVE_MIN_LEGS)
|
|
{
|
|
Print(ID + ": WARNING - only " + IntegerToString(ArraySize(legs)) + " confirmed swing legs in " +
|
|
IntegerToString(span) + " bars, too few to trust a median. Falling back to " +
|
|
IntegerToString(HISTORY_BARS_FALLBACK) + " bars.");
|
|
return HISTORY_BARS_FALLBACK;
|
|
}
|
|
double median = MathMedian(legs);
|
|
//--- snap DOWN to the ladder (see LEGACY_HISTORY_BARS_SLOT's comment for floor/cap rationale)
|
|
int ladder[] = {12, 16, 20, 24, 32};
|
|
int window = HISTORY_BARS_FLOOR;
|
|
for(int i = 0; i < ArraySize(ladder); i++)
|
|
if(ladder[i] <= median)
|
|
window = ladder[i];
|
|
PrintFormat("%s: derived input window - %d bars (median confirmed swing leg %.1f over %d legs in %d "
|
|
"bars, snapped down to the ladder%s). Measured once at model creation and pinned in the "
|
|
".cfg; an existing model adopts its own trained window instead.",
|
|
ID, window, median, ArraySize(legs), span,
|
|
median > 32 ? ", CAPPED at 32 - era time scales with the window" : "");
|
|
return window;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Dense-taper depth - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ComputeHiddenLayerCount(void) const
|
|
{
|
|
//--- Diagnostic escape hatch (compile-time, see ForceHiddenLayers). Deliberately not an input: this
|
|
//--- exists to run depth comparisons while working on the EA, and a user who picks a depth is
|
|
//--- contradicting the width and taper the code derived around it.
|
|
if(ForceHiddenLayers > 0)
|
|
return (int)MathMax(1, MathMin(MAX_HIDDEN_LAYERS, ForceHiddenLayers));
|
|
//--- Depth follows from the two ENDPOINTS the taper already has to connect - the derived first-
|
|
//--- layer width and the output-tied final hidden width (see BuildFreshTopology's taper block) -
|
|
//--- by asking how many steps it takes to get from one to the other at a sane per-layer
|
|
//--- compression ratio.
|
|
int lastHidden = (int)MathMax(HIDDEN_TAPER_OUTPUT_MULTIPLE * m_outputNeuronsCount, HIDDEN_TAPER_MIN_WIDTH);
|
|
lastHidden = (int)MathMin(lastHidden, m_initialNeuronsCount);
|
|
if(lastHidden <= 0 || m_initialNeuronsCount <= lastHidden)
|
|
return MIN_HIDDEN_LAYERS;
|
|
double steps = MathLog((double)m_initialNeuronsCount / (double)lastHidden) / MathLog(HIDDEN_TAPER_TARGET_RATIO);
|
|
int layers = (int)MathRound(steps) + 1; // +1: the first layer IS the starting endpoint, not a step
|
|
return (int)MathMax(MIN_HIDDEN_LAYERS, MathMin(MAX_HIDDEN_LAYERS, layers));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Conv output-filter count - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ComputeConvFilterCount(void) const
|
|
{
|
|
//--- AddConvStage sets window = ConvReceptiveFieldBars() * m_neuronsCount and step =
|
|
//--- m_neuronsCount, so each sliding position covers that many BARS of features and the layer is
|
|
//--- a learned projection from the whole window down to this many filters.
|
|
int chosen = (ConvReceptiveFieldBars() * m_neuronsCount) / CONV_COMPRESSION_DIVISOR;
|
|
//--- Snap DOWN to a power-of-two ladder for the same reason the first-layer width does: the target is
|
|
//--- approximate, and a value that moves with every feature toggle would re-key the weights file more
|
|
//--- often than the change in capacity justifies.
|
|
int ladder[] = {4, 8, 16, 32};
|
|
int snapped = CONV_FILTERS_MIN;
|
|
for(int i = 0; i < ArraySize(ladder); i++)
|
|
if(ladder[i] <= chosen)
|
|
snapped = ladder[i];
|
|
return (int)MathMax(CONV_FILTERS_MIN, MathMin(CONV_FILTERS_MAX, snapped));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Derived front-end stages, for the startup config line. |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalAIBase::FrontEndConfigSummary(void) const
|
|
{
|
|
string s = "";
|
|
//--- conv slides a ConvReceptiveFieldBars()-bar window one bar at a time, emitting m_convFilterCount
|
|
//--- filters per position; the optional channel pool + second conv follow. Reported from the shape
|
|
//--- helpers rather than re-derived, so this line always describes what AddConvStage actually built.
|
|
if(UsesConvStage())
|
|
{
|
|
s += " | conv " + IntegerToString(ConvReceptiveFieldBars()) + " bars x" +
|
|
IntegerToString(m_neuronsCount) + "->" + IntegerToString(m_convFilterCount) +
|
|
" (" + IntegerToString(ConvFirstStagePositions()) + " pos)";
|
|
if(HasSecondConvStage())
|
|
s += " | pool /" + IntegerToString(m_convFilterCount) +
|
|
" | conv2 ->" + IntegerToString(ConvOutputPositions()) + " pos x" +
|
|
IntegerToString(m_convFilterCount) + " = " + IntegerToString(ConvOutputWidth());
|
|
else
|
|
s += " = " + IntegerToString(ConvOutputWidth());
|
|
}
|
|
if(UsesLstmStage())
|
|
s += " | lstm " + IntegerToString(LstmFanIn()) + "->" + IntegerToString(m_lstmHiddenSize);
|
|
//--- The dense stack is budgeted against the RAW input, so on any topology with a front-end it can be
|
|
//--- WIDER than the vector reaching it - a linear fan-out that cannot recover information the
|
|
//--- bottleneck already discarded, only add parameters. Flag it rather than silently reshaping a
|
|
//--- trained topology; see ComputeFirstLayerWidth.
|
|
int frontEndOut = UsesLstmStage() ? m_lstmHiddenSize
|
|
: (UsesConvStage() ? ConvOutputWidth() : 0);
|
|
if(frontEndOut > 0 && m_initialNeuronsCount > frontEndOut)
|
|
s += " | NOTE dense fans out " + IntegerToString(frontEndOut) + "->" +
|
|
IntegerToString(m_initialNeuronsCount);
|
|
return s;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Input width the LSTM block actually receives. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::LstmFanIn(void) const
|
|
{
|
|
//--- LSTM-only: the layer sits directly on the input, so it sees the whole flattened vector.
|
|
//--- HYBRID: AddConvStage runs first, so the LSTM sees the CONV FEATURE MAP, not the input.
|
|
if(HasConvBeforeLstm())
|
|
return ConvOutputWidth();
|
|
return (int)m_historyBars * m_neuronsCount;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| LSTM recurrent hidden width - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ComputeLstmHiddenSize(void) const
|
|
{
|
|
//--- The LSTM block's parameter count is EXACTLY 4 * H * (H + inputs + 1) - see
|
|
//--- CNeuronLSTMOCL::SetInputs in AI\Network.mqh - and AddLstmStage feeds it the whole flattened
|
|
//--- input vector, so `inputs` is historyBars x neuronsCount.
|
|
int inputs = (LSTM_SEQUENCE_MODE ? (HasConvBeforeLstm() ? m_convFilterCount : m_neuronsCount)
|
|
: LstmFanIn());
|
|
double isBars = EstimatedInSampleBars();
|
|
if(inputs <= 0 || isBars <= 0.0)
|
|
return LSTM_HIDDEN_MIN;
|
|
double b = (double)(inputs + 1);
|
|
double budget = (-b + MathSqrt(b * b + 4.0 * (isBars / 4.0))) / 2.0;
|
|
int ladder[] = {8, 16, 32, 64, 128};
|
|
int snapped = LSTM_HIDDEN_MIN;
|
|
for(int i = 0; i < ArraySize(ladder); i++)
|
|
if((double)ladder[i] <= budget)
|
|
snapped = ladder[i];
|
|
return (int)MathMax(LSTM_HIDDEN_MIN, MathMin(LSTM_HIDDEN_MAX, snapped));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Capacity budget for the first dense layer - see the declaration. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ComputeFirstLayerWidth(void) const
|
|
{
|
|
//--- THE WIDTH THAT ACTUALLY REACHES THE DENSE STACK, not the raw input vector. Until 2026-08-09
|
|
//--- this budgeted against m_historyBars * m_neuronsCount on every topology, which is only the
|
|
//--- truth for a plain MLP.
|
|
int frontEndOut = UsesLstmStage() ? m_lstmHiddenSize
|
|
: (UsesConvStage() ? ConvOutputWidth() : 0);
|
|
//--- Same expression, one owner (see FirstLayerFanIn): the report in ReportDetectability has to
|
|
//--- charge for exactly what this decision charged for, or the two describe different networks.
|
|
int inputWidth = FirstLayerFanIn();
|
|
if(inputWidth <= 0)
|
|
return FIRST_LAYER_MIN_WIDTH;
|
|
double isBars = EstimatedInSampleBars();
|
|
//--- One first-layer weight per in-sample bar. That layer is (inputWidth+1) x width and dominates the
|
|
//--- model, so this is effectively a whole-model capacity budget. One parameter per sample is already
|
|
//--- generous for a signal this weak; it is a ceiling, not a target.
|
|
int budget = (int)(isBars / (double)(inputWidth + 1));
|
|
//--- Snap DOWN to the ladder: the estimate above is approximate, and a value that moves with every
|
|
//--- small change would re-key the weights file for no benefit. Rungs are far enough apart that the
|
|
//--- estimate would have to be wrong by ~2x to land on a different one.
|
|
int ladder[] = {16, 32, 64, 128, 256, 512, 1024};
|
|
int chosen = FIRST_LAYER_MIN_WIDTH;
|
|
for(int i = 0; i < ArraySize(ladder); i++)
|
|
if(ladder[i] <= budget)
|
|
chosen = ladder[i];
|
|
//--- NEVER WIDER THAN THE STAGE FEEDING IT. FrontEndConfigSummary() already calls that shape out
|
|
//--- as a defect when it happens; this stops it happening. The taper below this layer then
|
|
//--- funnels as intended.
|
|
if(frontEndOut > 0)
|
|
chosen = (int)MathMin(chosen, frontEndOut);
|
|
//--- Budget below the floor means this configuration cannot support even the narrowest usable
|
|
//--- layer - the model will be over-parameterized no matter what is chosen here, and no amount
|
|
//--- of regularization fixes having more weights than examples.
|
|
double lifespan = MeanLabelLifespan();
|
|
string basis = (lifespan > 1.0001)
|
|
? StringFormat("%.0f independent in-sample observations (%.0f bars / mean label"
|
|
" lifespan %.1f)", isBars, EstimatedInSampleBarsRaw(), lifespan)
|
|
: StringFormat("%.0f estimated in-sample bars (label overlap NOT YET MEASURED, so"
|
|
" this is an UPPER BOUND - see the CAPACITY line once labels exist)",
|
|
isBars);
|
|
if(budget < FIRST_LAYER_MIN_WIDTH)
|
|
Print(ID + ": WARNING - " + basis + " cannot support a " +
|
|
IntegerToString(inputWidth) + "-wide " +
|
|
(frontEndOut > 0 ? "vector into the dense stack" : "input") + ". The first layer is being floored at " +
|
|
IntegerToString(FIRST_LAYER_MIN_WIDTH) + " units, which is still roughly " +
|
|
DoubleToString((double)(inputWidth + 1) * FIRST_LAYER_MIN_WIDTH / MathMax(1.0, isBars), 1) +
|
|
" weights per independent observation - expect overfitting. Reduce HistoryBars or the" +
|
|
" feature set, lengthen the study period, pool instruments, or train on a lower timeframe.");
|
|
return MathMax(FIRST_LAYER_MIN_WIDTH, chosen);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Batch-normalization layer - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::AddBatchNormStage(CArrayObj *topology, int units)
|
|
{
|
|
if(CheckPointer(topology) == POINTER_INVALID)
|
|
return false;
|
|
//--- Not an error: the input is off, so the topology simply has no normalization layers. Returning
|
|
//--- true keeps every call site a plain `if(!Add...) return false;` with no extra branching.
|
|
if(!EnableBatchNorm)
|
|
return true;
|
|
//--- A window of 1 makes the layer a no-op passthrough (mean==x, variance==0), which is a silently
|
|
//--- useless layer rather than an obviously absent one. Refuse to build it instead.
|
|
if(BatchNormWindow <= 1)
|
|
return true;
|
|
CLayerDescription *desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
return false;
|
|
desc.count = units;
|
|
desc.type = defNeuronBatchNorm;
|
|
desc.batch = BatchNormWindow;
|
|
//--- Identity forward transform. The non-linearity belongs to the dense layer stacked on top of this
|
|
//--- one; normalizing and then squashing in the same step would undo the normalization.
|
|
desc.activation = NONE;
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
if(!topology.Add(desc))
|
|
{
|
|
delete desc;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Convolution front-end: conv -> channel pool -> conv. Shared by |
|
|
//| CSignalCONV and CSignalHYBRID - see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::AddConvStage(CArrayObj *topology)
|
|
{
|
|
if(CheckPointer(topology) == POINTER_INVALID)
|
|
return false;
|
|
//--- Stage 1: convolution across CONV_RECEPTIVE_FIELD_BARS bars, advancing one bar at a time.
|
|
CLayerDescription *desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
return false;
|
|
//--- desc.count here is the conv layer's own output-filter count (CNeuronConvOCL::Init's window_out
|
|
//--- param, AI\Network.mqh) - was m_hiddenLayersCount (an unrelated dense-taper-depth setting,
|
|
//--- defaulting to 4), bottlenecking every sliding position to just 4 filters regardless of how wide
|
|
//--- the rest of the network was. See ConvFilterCount's declaration comment (Variables\Inputs.mqh).
|
|
desc.count = m_convFilterCount;
|
|
desc.type = defNeuronConv;
|
|
// PRELU, not TANH: matches what CNeuronConv's CPU path (Network.mqh) has always hardcoded
|
|
// regardless of this setting (its activationFunction() override ignores `activation` entirely) -
|
|
// this used to silently diverge from the accelerated (OpenCL/CPU-DLL) tier, which DOES honor this
|
|
// field and was therefore actually running tanh instead of the intended PReLU whenever hardware
|
|
// accel was active.
|
|
desc.activation = PRELU;
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
//--- The whole point: a window spanning several bars. Guarded because m_historyBars can be small
|
|
//--- enough that a multi-bar window would not fit at all, in which case this degrades to the old
|
|
//--- per-bar projection rather than building a negative-width layer.
|
|
desc.window = ConvReceptiveFieldBars() * m_neuronsCount;
|
|
desc.step = m_neuronsCount;
|
|
if(!topology.Add(desc))
|
|
{
|
|
delete desc;
|
|
return false;
|
|
}
|
|
//--- NO POOL, and no second conv. It threw away 87.5% of this layer's output and starved every non-
|
|
//--- argmax filter of gradient. Springenberg et al. ICLR 2015.
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Conv chain shape. SINGLE SOURCE OF TRUTH - AddConvStage builds |
|
|
//| from these and LstmFanIn/FrontEndConfigSummary report from them, |
|
|
//| so what is constructed and what is logged cannot drift apart. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ConvReceptiveFieldBars(void) const
|
|
{
|
|
//--- Degrade to a per-bar projection rather than build an impossible layer when history is too short
|
|
//--- for a multi-bar window. MathMin against m_historyBars keeps window <= input width.
|
|
int bars = (int)MathMin((int)CONV_RECEPTIVE_FIELD_BARS, (int)m_historyBars);
|
|
return (bars > 0 ? bars : 1);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ConvFirstStagePositions(void) const
|
|
{
|
|
//--- Sliding positions of stage 1: window ConvReceptiveFieldBars() bars, step 1 bar.
|
|
int p = (int)m_historyBars - (ConvReceptiveFieldBars() - 1);
|
|
return (p > 0 ? p : 1);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::HasSecondConvStage(void) const
|
|
{
|
|
//--- Permanently false: the conv chain is ONE true convolution. Kept (rather than deleted along with the
|
|
//--- pool + second conv it used to gate) so ConvOutputPositions/ConvOutputWidth stay the single source of
|
|
//--- truth for the chain's shape and a future strided second stage has one place to switch itself on.
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ConvOutputPositions(void) const
|
|
{
|
|
int p = ConvFirstStagePositions();
|
|
return (HasSecondConvStage() ? p - (ConvReceptiveFieldBars() - 1) : p);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ConvOutputWidth(void) const
|
|
{
|
|
//--- Total element count reaching whatever is stacked above the conv chain: the conv output is
|
|
//--- position-major, window_out filters per position.
|
|
return ConvOutputPositions() * m_convFilterCount;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| LSTM sequence stage. Shared by CSignalLSTM and CSignalHYBRID - |
|
|
//| see the declaration comment. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::AddLstmStage(CArrayObj *topology)
|
|
{
|
|
if(CheckPointer(topology) == POINTER_INVALID)
|
|
return false;
|
|
CLayerDescription *desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
return false;
|
|
desc.count = m_lstmHiddenSize;
|
|
desc.type = defNeuronLSTM;
|
|
desc.activation = TANH;
|
|
//--- CNeuronLSTMOCL now has an accelerated SGD+momentum kernel (LSTM_UpdateWeightsMomentum,
|
|
//--- AI\Network.mqh/Network.cl/DirectML\WarriorCPU.cpp) alongside the original
|
|
//--- Adam one, so this layer honors the same TrainingOptimizer input as PAI/CONV - see
|
|
//--- m_optimizationAlgo's declaration comment.
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
//--- PER-TIMESTEP input width - the feature count for ONE bar as it reaches this layer. Note the
|
|
//--- step COUNT is the position count, which the conv chain shrinks below historyBars once a
|
|
//--- multi-bar window and a second conv are in play.
|
|
desc.window = (LSTM_SEQUENCE_MODE ? (HasConvBeforeLstm() ? m_convFilterCount : m_neuronsCount) : 0);
|
|
//--- MathMax(1,...) guard taken from the HYBRID copy: the CSignalLSTM copy divided unguarded, so a
|
|
//--- historyBars of 1 produced step 0 there and step 1 here for what is meant to be the same layer.
|
|
desc.step = MathMax(1, (int)m_historyBars / 2);
|
|
if(!topology.Add(desc))
|
|
{
|
|
delete desc;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Builds a fresh, untrained topology into Net - the exact layer |
|
|
//| construction InitNeuralNetwork() used to inline for the |
|
|
//| "no saved .nnw" case; factored out so TuneIndicatorsAndTrain() can|
|
|
//| get a clean-slate Net per trial without touching indicator init. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::BuildFreshTopology()
|
|
{
|
|
CArrayObj *Topology = new CArrayObj();
|
|
if(CheckPointer(Topology) == POINTER_INVALID)
|
|
return false;
|
|
//--- Input Layer
|
|
CLayerDescription *desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
//--- NetInputWidth = the bar window plus (meta target only) the per-candidate setup descriptor
|
|
//--- appended after it - see AppendCandidateFeatures. Zero-delta for every direction model.
|
|
desc.count = NetInputWidth();
|
|
desc.type = defNeuron;
|
|
desc.activation = NONE;
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
if(!Topology.Add(desc))
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
//--- neuron-type-specific layers (Conv+Pool, LSTM, or none for a plain perceptron)
|
|
if(!AddCustomLayers(Topology))
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
//--- Hidden Layers, tapering from m_initialNeuronsCount down to m_minNeuronsCount, each preceded
|
|
//--- by a batch-normalization layer (no-op when EnableBatchNorm is off). At 64 units, "keep 30%
|
|
//--- with a floor of 20" gives 64 -> 20 -> 20 - the reduction stops mattering after one step and
|
|
//--- the "minimum" silently becomes the width of every layer but the first.
|
|
int lastHidden = MathMax(HIDDEN_TAPER_OUTPUT_MULTIPLE * m_outputNeuronsCount, HIDDEN_TAPER_MIN_WIDTH);
|
|
//--- Never wider than where the taper starts: a narrow first layer (see the D1 case in
|
|
//--- ComputeFirstLayerWidth) must still funnel DOWN, not fan back out.
|
|
lastHidden = MathMin(lastHidden, m_initialNeuronsCount);
|
|
double taperRatio = (m_hiddenLayersCount > 1)
|
|
? MathPow((double)lastHidden / (double)m_initialNeuronsCount, 1.0 / (double)(m_hiddenLayersCount - 1))
|
|
: 1.0;
|
|
//--- Width of the layer immediately below the next batch-norm layer. Only advisory (CNet sizes
|
|
//--- each batch-norm layer from whatever it actually sits on), but kept honest so the descriptor
|
|
//--- list reads correctly.
|
|
int prevWidth = (int)(m_historyBars * m_neuronsCount);
|
|
bool result = true;
|
|
for(int i = 0; (i < m_hiddenLayersCount && result); i++)
|
|
{
|
|
int n = (i == 0)
|
|
? m_initialNeuronsCount
|
|
: MathMax(lastHidden, (int)MathRound(m_initialNeuronsCount * MathPow(taperRatio, (double)i)));
|
|
result = (AddBatchNormStage(Topology, prevWidth) && result);
|
|
if(!result)
|
|
break;
|
|
prevWidth = n;
|
|
desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
desc.count = n;
|
|
desc.type = defNeuron;
|
|
desc.activation = HiddenLayerActivation();
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
result = (Topology.Add(desc) && result);
|
|
}
|
|
if(!result)
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
//--- Batch norm immediately before the head. This is the one placement that matters most: it is what
|
|
//--- keeps the logit spread from decaying as the weights below it shrink, and it is the precondition
|
|
//--- for ever running an UNBOUNDED head here (see the 2026-07-28 note on desc.activation below).
|
|
if(!AddBatchNormStage(Topology, prevWidth))
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
//--- Output Layer
|
|
desc = new CLayerDescription();
|
|
if(CheckPointer(desc) == POINTER_INVALID)
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
desc.count = m_outputNeuronsCount;
|
|
desc.type = defNeuron;
|
|
//--- Never write the activation as a literal here: this line only ever reaches a BRAND-NEW
|
|
//--- topology, so a change made here never touches an existing .nnw (CNeuronBaseOCL::Save
|
|
//--- persists the activation and Load restores it).
|
|
desc.activation = OutputLayerActivation();
|
|
desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo;
|
|
if(!Topology.Add(desc))
|
|
{
|
|
delete Topology;
|
|
return false;
|
|
}
|
|
if(CheckPointer(Net) != POINTER_INVALID)
|
|
delete Net;
|
|
Net = new CNet(Topology);
|
|
delete Topology;
|
|
if(CheckPointer(Net) == POINTER_INVALID)
|
|
return false;
|
|
//--- A fresh topology invalidates any existing shadow (see m_shadowNet's declaration comment) -
|
|
//--- 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 (see the latch's
|
|
//--- declaration comment) - 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 (see OnlineLearnStep):
|
|
//--- 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;
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Creates the OHLC + ZigZag indicators the feature builder reads. |
|
|
//| Called by InitNeuralNetwork(), not by the framework - the public |
|
|
//| InitIndicators() override is the framework entry point. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitFeatureIndicators(CIndicators *indicators)
|
|
{
|
|
//--- Reset only the status label on (re-)init; deliberately do NOT PurgeChart() here so previously drawn
|
|
//--- signal arrows survive an EA re-init (recompile / param change / timeframe switch) instead of
|
|
//--- vanishing every time - see SIG_ARROW_PREFIX. Full cleanup still happens in the destructor.
|
|
ClearStatusLabel();
|
|
//--- NOTE: LoadChartSignals() is deliberately NOT called here any more. The mismatch made the
|
|
//--- restore silently no-op on every restart from the moment the fingerprint was introduced. Same
|
|
//--- family as the fingerprint trap documented at BuildConfigFingerprint: anything keyed on
|
|
//--- m_fileName must run AFTER it is fully built.
|
|
if(!InitOpen(indicators))
|
|
return false;
|
|
if(!InitClose(indicators))
|
|
return false;
|
|
if(!InitLow(indicators))
|
|
return false;
|
|
if(!InitHigh(indicators))
|
|
return false;
|
|
//--- label source, always created unconditionally, same as the OHLC indicators above - see
|
|
//--- m_ADZigZag's declaration comment. Optionally ALSO read as an input feature (m_useSwingContext,
|
|
//--- below) using the same already-running indicator instance - no separate init needed for that.
|
|
if(!InitADZigZag(indicators))
|
|
return false;
|
|
m_neuronsCount = 4; // (close-open)/atr, (high-open)/atr, (low-open)/atr, bullish/bearish flag
|
|
if(m_useVolumes)
|
|
{
|
|
// change ratio, level vs 50-bar baseline, absorption (range per unit volume), volume x range -
|
|
// see BufferTempDataCompute()'s matching block, and research/test_volume.py for the measurement
|
|
// that justified widening this from 1. m_neuronsCount is already in the config fingerprint, so
|
|
// this re-keys existing caches on its own: correct, the input vector genuinely changed shape.
|
|
m_neuronsCount += 4;
|
|
if(!InitVolumes(indicators))
|
|
return false;
|
|
}
|
|
// Unconditional, same reasoning as m_ATR/m_ADZigZag below: m_Time.GetData() is read
|
|
// unconditionally elsewhere (label-eligibility gate, cache anchor, online-learning watermark,
|
|
// arrow timestamps) regardless of whether the cyclical time-of-day/day-of-week values are also
|
|
// opted into as an explicit feature via m_useTime - so the indicator itself must always exist.
|
|
if(!InitTime(indicators))
|
|
return false;
|
|
if(m_useTime)
|
|
{
|
|
m_neuronsCount += 6;
|
|
}
|
|
if(m_useATR)
|
|
{
|
|
//already init in the base class
|
|
m_neuronsCount++;
|
|
}
|
|
if(m_useMA)
|
|
{
|
|
if(!InitMA(indicators))
|
|
return false;
|
|
m_neuronsCount += 5; // (open-MA)/atr, (high-MA)/atr, (low-MA)/atr, (close-MA)/atr, (MA-MA[1])/atr
|
|
}
|
|
if(m_useRSI)
|
|
{
|
|
if(!InitRSI(indicators))
|
|
return false;
|
|
m_neuronsCount++; // RSI/100
|
|
}
|
|
if(m_useMACD)
|
|
{
|
|
if(!InitMACDFeature(indicators))
|
|
return false;
|
|
m_neuronsCount += 3; // main/atr, signal/atr, histogram/atr
|
|
}
|
|
if(m_useIchimoku)
|
|
{
|
|
if(!InitIchimoku(indicators))
|
|
return false;
|
|
// (close-Tenkan)/atr, (close-Kijun)/atr, (Tenkan-Kijun)/atr, (close-SpanA)/atr, (close-SpanB)/atr,
|
|
// signed cloud thickness at this bar, signed PROJECTED cloud thickness, Chikou displacement
|
|
m_neuronsCount += 8;
|
|
}
|
|
if(m_useSwingContext)
|
|
m_neuronsCount += 9; // 5 confirmed-pivot features (direction, distance-since-pivot, prior-leg magnitude, retracement ratio, bars-since-pivot) + 4 recent-context features (Donchian pos 20/50, 20-bar return, 20-bar SMA extension) - see BufferTempDataCompute()'s matching block
|
|
if(m_useNews)
|
|
m_neuronsCount += 2; // NewsRecency, NewsProximity - see BufferTempDataCompute()'s matching block
|
|
if(m_useSpreadFeature)
|
|
m_neuronsCount += 2; // spread/ATR (volatility-regime reading), spread change ratio
|
|
// - see BufferTempDataCompute()'s matching block
|
|
if(m_useCrossAsset)
|
|
m_neuronsCount += CROSSASSET_FEATURES; // FX: base/quote strength + divergence; index: denom/risk-proxy strength
|
|
// divergence, cross-sectional dispersion - System\CrossAsset.mqh
|
|
if(m_useADCumulativeDelta)
|
|
{
|
|
if(!InitADCumulativeDelta(indicators))
|
|
return false;
|
|
m_neuronsCount += 6; // Pressure, CumulativeDelta, BullishPressure, BearishPressure, Absorption, Initiative
|
|
}
|
|
if(m_useADShorteningOfThrust)
|
|
{
|
|
if(!InitADShorteningOfThrust(indicators))
|
|
return false;
|
|
m_neuronsCount += 4; // SOT, SOTEffortRegime, SOTConfirmation, SOTPushRegime
|
|
}
|
|
if(m_useADWyckoffEventStream)
|
|
{
|
|
if(!InitADWyckoffEventStream(indicators))
|
|
return false;
|
|
// 16 = 13 buffers - EventPrice (buffer 4, excluded; see BufferTempDataCompute()'s comment) with
|
|
// THREE of them split into a direction/magnitude pair each: EventCode, EventPhase and
|
|
// StructuralPhase are signed categoricals, so 13 readings now occupy 16 inputs (2026-08-09 audit,
|
|
// N1 - see the split in BufferTempDataCompute for why). No reading was added or dropped.
|
|
m_neuronsCount += 16; // eventDir, eventStage, livePhaseDir, livePhaseMag, ZoneTop, ZoneBottom, structDir, structMag, CHoCHTrendToRange, CHoCHRangeToTrend, SlopeAccumulationBullish, SlopeAccumulationBearish, SlopeDistributionBullish, SlopeDistributionBearish, Reaccumulation, Redistribution
|
|
}
|
|
if(m_useADWyckoffFailedStructure)
|
|
{
|
|
if(!InitADWyckoffFailedStructure(indicators))
|
|
return false;
|
|
m_neuronsCount += 5; // Value, BullishStructuralFailure, BearishStructuralFailure, FailedAccumulation, FailedDistribution
|
|
}
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
{
|
|
if(!InitADWyckoffSignificantBarInversion(indicators))
|
|
return false;
|
|
m_neuronsCount += 5; // SignificantBarQuality, BullishSignificantBar, BearishSignificantBar, BullishControlFlip, BearishControlFlip
|
|
}
|
|
//--- ALT DATA (2026-08-16). Externally collected, publication-stamped features (COT positioning,
|
|
//--- VIX complex, macro) exported by research/altdata/export.py into
|
|
//--- Common\Files\Warrior_EA\AltData\{SYMBOL}_{TF}.csv - see System\AltData.mqh for the
|
|
//--- lookahead/degradation contracts.
|
|
if(m_altDataEnabled)
|
|
{
|
|
string altPin = ReadAltDataPinFromCfg();
|
|
if(altPin != "")
|
|
m_altData.SetPinnedNames(altPin);
|
|
m_altData.Load(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period); // logs its own outcome; absence is normal
|
|
m_altDataNamesPinned = (altPin != "") ? altPin : m_altData.NamesCsv(); // fresh model: stamped by the first .cfg save
|
|
m_useAltData = (m_altData.FeatureCount() > 0);
|
|
if(m_useAltData)
|
|
m_neuronsCount += m_altData.FeatureCount();
|
|
}
|
|
else
|
|
{
|
|
//--- Operator opt-out (EnableAltData=false): zero width, nothing pinned. On a model trained
|
|
//--- WITH alt features this shrinks neuronsCount, mismatches the .cfg compare and correctly
|
|
//--- starts fresh - stated in the input's comment rather than silently absorbed.
|
|
m_useAltData = false;
|
|
m_altDataNamesPinned = "";
|
|
}
|
|
if(!FolderCreate(m_folderPath, FILE_COMMON))
|
|
{
|
|
if(GetLastError() != 5010) // If the error is not because the folder already exists
|
|
{
|
|
Print("Failed to create folder: " + m_folderPath);
|
|
}
|
|
else
|
|
{
|
|
ResetLastError(); // Reset the error code
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|