refactor(topology): split shape derivation into CTopology, leave the boot sequence in place
Expert/AIBase/Topology.mqh (1191 lines) held two genuinely different jobs: the
fingerprint/derived-shape/BuildFreshTopology math, and InitNeuralNetwork/
InitFeatureIndicators - the network boot sequence (config-lock, tester-cache
seeding, load/save the .cfg, net-load backend fallback, chart/persistence/
online-learning orchestration).
Extracted the first job to Expert/Topology/ as CTopology + CTopologyView/
CAIBaseTopologyView (20 methods: BuildModelFingerprint, the Estimated*/Compute*
budget math, the Conv*/Lstm* shape helpers, Add*Stage, BuildFreshTopology).
STATELESS, like ModelPersistence - grep-verified zero exclusive fields, every
member these methods touch is shared elsewhere in the signal. Reused ~15
existing Data*/Chart*/Persist*/Exc* getters per the established convention;
added ~20 new getter overloads next to their existing setters (UseVolumes(),
MinDirectionalRecall(), etc. - same pattern as SignalClusterWindow) and ~16
new Topology*() wrappers for fields with no prior accessor. The Net-pointer
swap in BuildFreshTopology is one consolidated view call
(TopologyReplaceNetFromTopology), same doctrine as Persistence's
RunCpuInferenceSelfCheck - irreducible pointer work, not signal state.
Deliberately did NOT extract InitNeuralNetwork/InitFeatureIndicators: they
orchestrate nearly every other collaborator (chart, persistence, online-
learning, cross-asset, config-lock) rather than deriving a shape, so moving
them would just relocate a hub, not reduce coupling - same judgment call as
Inference.mqh (assessed, not extracted). They stay in the AIBase/Topology.mqh
partial, byte-identical to before (diffed against git HEAD to confirm), and
now call the extracted math through the same public forwards every other
caller already used.
Verified: string- and numeric-literal diff of the old file's 20 method bodies
against the new CTopology methods (0 differences), InitNeuralNetwork/
InitFeatureIndicators byte-identical, self-compiled 0 errors/0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 23:23:00 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//+------------------------------------------------------------------+
2026-08-01 11:27:28 -04:00
//| Topology.mqh |
//| |
refactor(topology): split shape derivation into CTopology, leave the boot sequence in place
Expert/AIBase/Topology.mqh (1191 lines) held two genuinely different jobs: the
fingerprint/derived-shape/BuildFreshTopology math, and InitNeuralNetwork/
InitFeatureIndicators - the network boot sequence (config-lock, tester-cache
seeding, load/save the .cfg, net-load backend fallback, chart/persistence/
online-learning orchestration).
Extracted the first job to Expert/Topology/ as CTopology + CTopologyView/
CAIBaseTopologyView (20 methods: BuildModelFingerprint, the Estimated*/Compute*
budget math, the Conv*/Lstm* shape helpers, Add*Stage, BuildFreshTopology).
STATELESS, like ModelPersistence - grep-verified zero exclusive fields, every
member these methods touch is shared elsewhere in the signal. Reused ~15
existing Data*/Chart*/Persist*/Exc* getters per the established convention;
added ~20 new getter overloads next to their existing setters (UseVolumes(),
MinDirectionalRecall(), etc. - same pattern as SignalClusterWindow) and ~16
new Topology*() wrappers for fields with no prior accessor. The Net-pointer
swap in BuildFreshTopology is one consolidated view call
(TopologyReplaceNetFromTopology), same doctrine as Persistence's
RunCpuInferenceSelfCheck - irreducible pointer work, not signal state.
Deliberately did NOT extract InitNeuralNetwork/InitFeatureIndicators: they
orchestrate nearly every other collaborator (chart, persistence, online-
learning, cross-asset, config-lock) rather than deriving a shape, so moving
them would just relocate a hub, not reduce coupling - same judgment call as
Inference.mqh (assessed, not extracted). They stay in the AIBase/Topology.mqh
partial, byte-identical to before (diffed against git HEAD to confirm), and
now call the extracted math through the same public forwards every other
caller already used.
Verified: string- and numeric-literal diff of the old file's 20 method bodies
against the new CTopology methods (0 differences), InitNeuralNetwork/
InitFeatureIndicators byte-identical, self-compiled 0 errors/0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 23:23:00 -04:00
//| The network BOOT SEQUENCE: InitNeuralNetwork() (indicator init, |
//| config-lock, tester-cache seeding, load/save the .cfg, net load |
//| with backend fallback, chart-signal restore, arm the first study |
//| event) and InitFeatureIndicators() (the ~15 per-feature indicator|
//| Init* calls that size m_neuronsCount). This is orchestration |
//| across nearly every other collaborator - chart, persistence, |
//| online-learning, cross-asset - not shape derivation, so it stays |
//| a raw-include partial of CExpertSignalAIBase rather than moving |
//| into a collaborator of its own; see project_oop_module_pattern |
//| memory for the "diagnose before applying the pattern" doctrine |
//| this follows. The FINGERPRINT, the derived shape (width/taper/ |
//| depth/conv filters/LSTM hidden), the conv/LSTM/batch-norm stages |
//| and BuildFreshTopology are a genuinely separable job and now |
//| live in Expert\Topology\Topology.mqh as CTopology, reached |
//| through the one-line forwards on CExpertSignalAIBase. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
# ifndef WARRIOR_AIBASE_TOPOLOGY_MQH
# define WARRIOR_AIBASE_TOPOLOGY_MQH
2026-08-19 23:50:30 -04:00
//+------------------------------------------------------------------+
//| 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 ;
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
if ( ! InitFeatureIndicators ( indicators ) )
2026-08-19 23:50:30 -04:00
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 ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-19 23:50:30 -04:00
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 ( ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-19 23:50:30 -04:00
string fp = BuildModelFingerprint ( ) ;
2026-08-01 11:27:28 -04:00
//--- 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
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
string cfgTag = " [ " + m_id + " - " + StringSubstr ( StringFormat ( " %08x " , fpHash ) , 0 , 4 ) + " ] " ;
if ( StringFind ( ID , cfgTag ) < 0 )
ID + = cfgTag ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
Print ( ID + " : config - " + IntegerToString ( m_hiddenLayersCount ) + " dense from " +
IntegerToString ( m_initialNeuronsCount ) + " units | batchnorm " +
( ( EnableBatchNorm & & BatchNormWindow > 1 ) ? " ON( " + IntegerToString ( BatchNormWindow ) + " ) " : " OFF " ) +
2026-08-24 19:33:37 -04:00
//--- "requested", not effective: the delivered tau is capped against the head's usable
//--- logit range and cannot be known until the class priors are measured -
//--- ApplyLogitAdjustment logs the value in force.
" | class-imbalance logit-adjust(tau 1.00 requested) " +
2026-08-01 11:27:28 -04:00
" | input " + IntegerToString ( ( int ) m_historyBars * m_neuronsCount ) +
" ( " + IntegerToString ( ( int ) m_historyBars ) + " bars x " + IntegerToString ( m_neuronsCount ) + " ) " +
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
FrontEndConfigSummary ( ) ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 ;
2026-08-22 00:25:52 -04:00
//--- Claim these files before anything reads or writes them, and refuse to start if another
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
//--- chart in this terminal already holds them (see AcquireConfigLock).
if ( ! inTesterOrOpt & & ! AcquireConfigLock ( ) )
2026-08-01 11:27:28 -04:00
return false ;
2026-08-22 00:25:52 -04:00
//--- Any Strategy-Tester run - a single backtest OR an optimization pass - runs pure inference
//--- on the deployed model, never trains.
2026-08-01 11:27:28 -04:00
m_inferenceOnly = MQLInfoInteger ( MQL_TESTER ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 ) )
{
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
if ( CopyFileWithRetry ( m_fileName + " .nnw " , m_activeFileName + " .nnw " ) )
{
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 )
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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. " ) ;
}
fix(topology): stop a training-alone size becoming permanent, and stop the keep-screen latching underpowered
1. THE POOL FIX WAS LANDING ON A TOPOLOGY THAT COULD NOT SEE IT.
ComputeFirstLayerWidth budgets against EstimatedInSampleBars, which counts
this chart's own bars PLUS the training pool. On a COLD fleet start every
chart derives and pins its topology BEFORE any chart has published a pool
file - measured on the 18:13 start, model creation at 18:13:21 against a
first publish at 18:13:48. All six sized as if training alone, wrote that
into .cfg, and adopted it back on every later start even with the pool full.
SP500 ran a first layer floored to 16 while adopting 30229 peer rows.
Adopt-don't-compare exists to protect weights shaped by those sizes. It was
also running for a model with NO .nnw, where there is nothing to protect and
the .cfg is just a record of one unlucky moment. The four derived sizes are
now re-measured when no weights exist.
Safe on all three counts that matter: free (nothing to discard), cannot loop
(once weights exist the .cfg is authoritative again), and cannot fragment the
pool - the derived width is NOT in BuildModelFingerprint, which keys only on
the FEATURE layout. Verified: field 2 of the fingerprint is
LEGACY_HISTORY_BARS_SLOT, not the first-layer width.
TO TAKE EFFECT the weights must be wiped while the TrainPool is KEPT - the
census has to be non-empty at derivation time. A full wipe empties the pool
and reproduces the original condition exactly.
2. THE KEEP-SCREEN LATCHED ON AN UNDERPOWERED SAMPLE.
MI_MIN_SAMPLES is a floor for "can this be computed", and it was being used
as the bar for "is this answer final". The screen fired on the first era
clearing 200 rows and latched, measuring at 202-773 samples where a warm
chart gives ~2065. Columns kept then tracked SAMPLE SIZE rather than
information - EURUSD kept 0 of 49 at n=202, SP500 kept 15 at n=773, and the
ordering across all six charts was very nearly monotone in n.
A thin sample is still measured and printed, but it no longer closes the
question: below MI_GOOD_SAMPLE_FRACTION of the target the result is labelled
underpowered and a later era supersedes it, bounded by the same attempt
budget. An underpowered screen that latches is worse than one that waits,
because it looks like a result.
Build tag -> fleet-pool-v2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:27:48 -04:00
//--- ADOPT-DON'T-COMPARE PROTECTS TRAINED WEIGHTS. WITH NO WEIGHTS THERE IS NOTHING TO PROTECT.
//---
//--- The .cfg pins four DERIVED sizes (first-layer width, depth, conv filters, LSTM hidden), and
//--- adopting them is right for a model that has weights shaped by them. It was also being done
//--- for a model with NO .nnw at all, and that turned one unlucky moment into a permanent
//--- property of the fleet:
//---
//--- ComputeFirstLayerWidth budgets against EstimatedInSampleBars, which counts this chart's own
//--- bars PLUS the training pool. On a COLD fleet start every chart derives its topology before
//--- any chart has published a pool file - measured 2026-08-26, model creation 18:13:21 against a
//--- first publish at 18:13:48 - so all six sized as if training alone, wrote that into .cfg, and
//--- then adopted it back on every subsequent start even though the pool had been full for hours.
//--- SP500 sat at a first layer floored to 16 while adopting 30229 peer rows.
//---
//--- Re-deriving when there are no weights is free (nothing to discard), cannot loop (once weights
//--- exist the .cfg is authoritative again), and cannot fragment the pool: the derived width is
//--- NOT part of BuildModelFingerprint, which keys only on the FEATURE layout.
bool haveWeights = FileIsExist ( m_activeFileName + " .nnw " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
bool cfgAdopted = haveWeights & &
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 ) ;
if ( ! haveWeights & & FileIsExist ( m_activeFileName + " .cfg " , m_activeFileCommon ? FILE_COMMON : 0 ) )
Print ( __FUNCTION__ + " : " + ID + " - a .cfg exists but no weights do, so its DERIVED sizes were "
" re-measured rather than adopted (first layer " + IntegerToString ( m_initialNeuronsCount ) +
" , depth " + IntegerToString ( m_hiddenLayersCount ) + " ). A .cfg written before the training "
" pool had any peer files would otherwise pin a training-alone topology forever. " ) ;
if ( ! cfgAdopted )
2026-08-01 11:27:28 -04:00
{
2026-08-22 00:25:52 -04:00
//--- 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).
2026-08-01 11:27:28 -04:00
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 ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 ) ;
2026-08-22 00:25:52 -04:00
//--- and the pattern-database backfill marker (see StartPatternDatabaseBackfill): it records
//--- the era of the model whose OOS calls were written into the ranking tables.
fix: the DB backfill could never run, and HEAD did not compile
Four defects in 64c5dd5/1a05e63, found by review + a baseline compile.
Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable.
1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were
declared `virtual bool ... override`, but CAppDialog declares both as
`virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151
on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was
never a success flag to forward. Verified: 0 errors, 0 warnings.
2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual
simulation (that one has been dead since it was written). Both are armed
at the instant convergence is declared, and both advance only from inside
Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick
ArmStudyEvent site sits in the `else` of a branch taken whenever
m_trainingComplete is set and m_trainRunActive is clear - which is exactly
the state FinalizeTrainRun() leaves behind one line before they are armed.
Train() was never called again, so the walks sat at their start index
forever: no "simulation complete" line, and not one row written to the DB
this feature exists to fill. Only a manual Resume/Retrain unstuck them.
Both flags now keep the model schedulable.
3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed.
Ensemble members deploy at Train() ENTRY and return immediately (so no era
is wasted), which skips the era-end block the backfill was started from.
All four members were a no-op for a second, independent reason. Armed on
the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff.
4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key,
no duplicate check - and m_dbBackfillDone is in-memory, so every later
attach that retrained to convergence wrote a second full set of rows for
the same bars. The ranking would count one bar once per model that ever
deployed, weighting superseded opinions as heavily as the live one. A
.dbfill marker stamps the deployed era; written only on completion (an
interrupted walk redoes itself rather than ranking a partial window) and
deleted with the other sidecars on reset-weights.
Also: WarmBlocking's timeout was silent, which restored the exact silent
pin failure it was added to prevent - it now says so in the journal, and
returns true for "no reference pairs to wait for" so the warning stays rare
enough to be read.
Not addressed, needs a decision: the backfill scores the OOS window with the
checkpoint that was SELECTED as best on that same window, then writes those
win rates into the table filter weights rank on - the selection set consumed
twice, undiscounted, while the deploy gate right next to it applies a
family-wise correction for exactly that effect. The rows are also simulated
triple-barrier outcomes at today's spread sharing a table with realised
fills. The completion log line now states both plainly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:25:51 -04:00
if ( FileIsExist ( m_activeFileName + " .dbfill " , m_activeFileCommon ? FILE_COMMON : 0 ) )
FileDelete ( m_activeFileName + " .dbfill " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
2026-08-01 11:27:28 -04:00
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 ( ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 )
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
{
2026-08-01 11:27:28 -04:00
LoadModelStats ( m_activeFileName , m_activeFileCommon ) ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- ...and rebuild the ensemble headline from whatever record that just restored. Without this
//--- a reloaded DEPLOYED chart had no aggregate line at all: the only other caller runs at
//--- pass-3 completion, and a deployed ensemble runs no further eras to reach it - so the panel
//--- fell back to one row per member, which is exactly the readout the operator asked to be rid
//--- of. No "this era" figure is passed: there has not been one this session, and showing the
//--- stored era's number here would read as live.
if ( m_ensembleMember )
PublishEnsembleAccuracyLine ( -1.0 , 0 ) ;
fix(vote): persist the tier ladder - a converged model was mute after every restart
THIS IS NOT A DISPLAY BUG. A deployed model could not vote, or trade, at
any point after a terminal restart, and never would have.
LiveVoteContribution() returns 0 for every call until m_tiersSelfRanked
is set - deliberately, and correctly: before RankTiersFromOos() runs,
m_pattern_0..3 hold the constructor's stock 25/50/75/100, which since the
2026-08-18 currency change is the WRONG UNIT rather than a weak opinion,
and one unranked member would drag the whole ensemble over any threshold.
But that ladder is produced ONLY by a completed pass 3, and it was never
persisted - the code comment at LiveVoteContribution says so outright.
A converged model runs no further passes. So on every restart it lost its
entire vote permanently:
LiveVoteContribution -> 0 => no live vote ("0 vote/4 flat")
ReconstructionWeight -> 0 => overlay divisor 0 ("0 had a snapshot")
=> no arrows
=> no fired bars, so g_ensCumOosTotal stays 0
=> "measuring..." forever
Every symptom reported over the last three exchanges is that one cause.
The log is unambiguous: six H4 charts resumed at era 70/71, all 24
rescans completed with ~2700 Buy / ~2200 Sell per model, and the overlay
then swept 4999 bars finding "0 had a snapshot". The calls were there;
nothing was permitted to count them.
WST7 now stores the four tier weights, the module trust weight and the
self-ranked flag beside the model. Restored only when the stored flag
says the ladder was MEASURED - a .stats written before a model's first
pass 3 holds the stock ladder, and adopting that as if measured is the
exact error the flag exists to prevent.
A .stats predating WST7 has no ladder, so existing converged models stay
silent until their next scoring pass mints one. That case now prints a
warning naming all three of its symptoms, because each one independently
looks like a different bug.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 14:26:33 -04:00
//--- CONVERGED BUT MUTE. Say so, loudly and once, because every downstream symptom of it looks
//--- like something else: no arrows reads as a drawing fault, "0 vote/4 flat" reads as models
//--- that disagree, and "measuring..." reads as a panel that has not caught up. All three are
//--- the same thing - a model with no measured tier ladder returns 0 from LiveVoteContribution
//--- by design, so it cannot vote, cannot be counted in the reconstruction divisor, and cannot
//--- contribute to the aggregate win rate. A .stats written before WST7 has no ladder in it.
if ( m_trainingComplete & & ! m_tiersSelfRanked )
Print ( ID + " : WARNING - resumed CONVERGED but with NO MEASURED TIER LADDER, so this model "
" CANNOT VOTE and will place no trades. The tier weights are produced only by a "
" completed scoring pass and were not stored by the build that trained this model "
" (.stats predates WST7). It will mint and store them at the end of its next scoring "
" pass, after which restarts keep them. Until then this member is silent - that is the "
" cause of an empty chart, a '0 vote' readout and a 'measuring...' win rate, all three. " ) ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
}
2026-08-01 11:27:28 -04:00
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. " ) ;
2026-08-13 10:23:11 -04:00
//--- RESUMED MODELS GET THE SAME WARM-UP AS FRESH ONES (2026-08-13; was `netLoaded ? 0 : 3`).
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-13 10:23:11 -04:00
m_warmupPassesRemaining = 3 ;
2026-08-01 11:27:28 -04:00
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 )
{
2026-08-22 00:25:52 -04:00
//--- Restart deploying previously AutoTune-d indicator params even with
//--- AutoTuneIndicators=false now.
2026-08-13 10:23:11 -04:00
AdoptIndicatorParams ( loadedIndicatorParams , indicators ) ;
2026-08-01 11:27:28 -04:00
}
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
2026-08-22 00:25:52 -04:00
//--- (OpenCL-not-found) left by the compute probe inside CNet::Load, NOT the reason the file
//--- was rejected.
2026-08-01 11:27:28 -04:00
if ( error_code ! = 5004 ) // not "file not found"
ResetLastError ( ) ;
2026-08-22 00:25:52 -04:00
//--- 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).
2026-08-01 11:27:28 -04:00
m_trainingComplete = false ;
m_eraCount = 0 ;
dtStudied = 0 ;
dForecast = 0 ;
2026-08-22 00:25:52 -04:00
//--- 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).
2026-08-01 11:27:28 -04:00
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. " ) ;
}
2026-08-22 00:25:52 -04:00
//--- Re-seed before building a fresh topology so weight init is genuinely random. See
//--- System\Random.mqh. Matches ResetWeights() and OnInit.
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:
WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.
SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.
CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.
AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.
Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.
The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
WarriorRandSeed ( ID ) ;
2026-08-01 11:27:28 -04:00
//--- Era 0 with no weights behind it, so any arrow currently on this chart was drawn by a
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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 )
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
ArmStudyEvent ( ( long ) MathMax ( 0 , MathMin ( iTime ( _Symbol , PERIOD_CURRENT , ( int ) ( 100 * Net . recentAverageSmoothingFactor * ( m_trainingComplete ? 1 : 10 ) ) ) , dtStudied ) ) , " Init " ) ;
2026-08-01 11:27:28 -04:00
//--- 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 ;
return true ;
}
//+------------------------------------------------------------------+
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
//| 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. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
bool CExpertSignalAIBase : : InitFeatureIndicators ( CIndicators * indicators )
2026-08-01 11:27:28 -04:00
{
//--- 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 ( ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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
2026-08-24 18:26:25 -04:00
//--- m_zigZag's declaration comment. Optionally ALSO read as an input feature (m_useSwingContext,
2026-08-01 11:27:28 -04:00
//--- below) using the same already-running indicator instance - no separate init needed for that.
2026-08-24 18:26:25 -04:00
if ( ! InitZigZag ( indicators ) )
2026-08-01 11:27:28 -04:00
return false ;
m_neuronsCount = 4 ; // (close-open)/atr, (high-open)/atr, (low-open)/atr, bullish/bearish flag
if ( m_useVolumes )
{
feat(ai): widen the volume feature block from 1 value to 4
The block fed exactly one number: (v[i] - v[i-1]) / v[i-1]. That is the first difference,
and it cannot express three things that matter - the LEVEL relative to a baseline (two
dead bars and two frantic bars both read ~0 change), and the two volume-vs-range
interactions, where heavy participation that went NOWHERE (absorption) and heavy
participation that travelled (continuation) mean opposite things and currently collapse
onto the same value.
research/test_volume.py measures each candidate's mutual information with the triple-
barrier label across 3 instruments x 2 geometries, against a BLOCK-permutation null -
blocks sized to the barrier horizon, because adjacent labels share almost their entire
outcome window and a free shuffle yields a null so tight that everything looks
significant. Finite-sample MI bias (~7/n here) is reported alongside rather than
subtracted, since the permutation null already absorbs it.
Result: volLevel beats the shipped change ratio outright on 4 of 6 cells (EURUSD 2:3
+0.000118 excess at p=0.006, USDJPY 1:2 +0.000284 at p=0.002); absorption is the single
strongest reading anywhere in the sweep at EURUSD 1:2 (+0.000404, p=0.002) though it is
null on XAUUSD; vol x range clears on 4 of 6. The shipped change ratio is itself
significant on 5 of 6, so it stays.
Kept OUT: a session-relative z-score against the same hour-of-day's own recent history.
It was the weakest candidate - null on both EURUSD cells - and it is the only one needing
per-hour rolling bookkeeping in MQL5. Not worth the state for a reading that did not
survive its own null on the primary instrument.
Magnitudes, stated plainly because they are the point: the excess MI is ~2e-4 nats against
a label entropy near 1.05. That is under a tenth of one percent of the label's
uncertainty. It is real, it repeats across instruments, and it is nowhere near an edge -
this is worth having because it costs one 50-bar loop, not because it changes the answer.
Prior work stands: the whole single-series feature family measured at the noise floor.
m_neuronsCount is already in the fingerprint, so the width change re-keys existing caches
by itself, which is correct - the input vector genuinely changed shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:34:33 -04:00
// 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 ;
2026-08-01 11:27:28 -04:00
if ( ! InitVolumes ( indicators ) )
return false ;
}
2026-08-24 18:26:25 -04:00
// Unconditional, same reasoning as m_ATR/m_zigZag below: m_Time.GetData() is read
2026-08-01 11:27:28 -04:00
// 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_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
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
if ( m_useSpreadFeature )
m_neuronsCount + = 2 ; // spread/ATR (volatility-regime reading), spread change ratio
// - see BufferTempDataCompute()'s matching block
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
if ( m_useCrossAsset )
2026-08-11 21:07:52 -04:00
m_neuronsCount + = CROSSASSET_FEATURES ; // FX: base/quote strength + divergence; index: denom/risk-proxy strength
2026-08-16 13:39:00 -04:00
//--- 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
2026-08-22 00:25:52 -04:00
//--- lookahead/degradation contracts.
2026-08-16 15:12:54 -04:00
if ( m_altDataEnabled )
{
feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.
1. SP500 was training alone, and one alt-data column was the reason.
The alt block's width joins the model fingerprint, and the pool reader only
adopts peer rows whose fingerprint and width match. The exporter gives each
instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
13 - so the fleet ran as three incompatible pools:
EURUSD/USDJPY/USDCAD adopt ~57-60k peer rows each
XAUUSD/XTIUSD adopt 6.4k / 20.3k
SP500 "EVERY peer file was REJECTED, so this chart is
training alone" - 0 rows
SP500 therefore trained on 2279 independent observations against a 600-wide
input with its first layer floored at 16, printing its own "expect
overfitting" warning. It is the one chart with no pool and the worst
capacity ratio in the fleet by a factor of three.
Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
instead of their own file header. An existing model still adopts its .cfg
pin, so this re-keys nothing that is already trained.
Intersection rather than union: filling an absent series with its median
makes that column constant per instrument, which lets a pooled model
identify the source instrument and stop learning the shared mechanism. It
is also 6 columns narrower. Cost is six columns whose retained information
is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
to names.
2. The MI keep-screen disabled itself for the whole run on any cold start.
ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
the label cache is allocated before it is filled, so BuildMiSample finds no
row carrying a resolved label and returns 0 - a sixth exit, and the only
one the 8c1266d instrumentation did not cover, which is why it printed
nothing. observed then stayed -1, the permutation loop never iterated, and
the report emitted "-1.00000 nats over 0 permutations" beside a plausible
"strongest single feature 0.05979" that was a STALE m_miBestColumn from an
earlier scoring call. The first ensemble member propagated the latch to
g_ensembleChartMiReportDone and silenced every member on the chart.
The flag now latches only once a measurement exists. A short sample is
reported as a deferral naming the two numbers that identify it (cached bars
vs bars carrying a resolved label) and retried, up to
MI_REPORT_MAX_ATTEMPTS.
Build tag -> fleet-pool-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
//--- A MODEL'S OWN .cfg STILL WINS. Adopt-don't-compare: an existing model keeps the column set
//--- its weights were trained against, exactly as it keeps its topology. Only a FRESH model
//--- takes the fleet set - which is what makes this change retrain-forcing rather than
//--- silently re-keying a trained model's inputs.
2026-08-16 15:12:54 -04:00
string altPin = ReadAltDataPinFromCfg ( ) ;
feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.
1. SP500 was training alone, and one alt-data column was the reason.
The alt block's width joins the model fingerprint, and the pool reader only
adopts peer rows whose fingerprint and width match. The exporter gives each
instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
13 - so the fleet ran as three incompatible pools:
EURUSD/USDJPY/USDCAD adopt ~57-60k peer rows each
XAUUSD/XTIUSD adopt 6.4k / 20.3k
SP500 "EVERY peer file was REJECTED, so this chart is
training alone" - 0 rows
SP500 therefore trained on 2279 independent observations against a 600-wide
input with its first layer floored at 16, printing its own "expect
overfitting" warning. It is the one chart with no pool and the worst
capacity ratio in the fleet by a factor of three.
Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
instead of their own file header. An existing model still adopts its .cfg
pin, so this re-keys nothing that is already trained.
Intersection rather than union: filling an absent series with its median
makes that column constant per instrument, which lets a pooled model
identify the source instrument and stop learning the shared mechanism. It
is also 6 columns narrower. Cost is six columns whose retained information
is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
to names.
2. The MI keep-screen disabled itself for the whole run on any cold start.
ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
the label cache is allocated before it is filled, so BuildMiSample finds no
row carrying a resolved label and returns 0 - a sixth exit, and the only
one the 8c1266d instrumentation did not cover, which is why it printed
nothing. observed then stayed -1, the permutation loop never iterated, and
the report emitted "-1.00000 nats over 0 permutations" beside a plausible
"strongest single feature 0.05979" that was a STALE m_miBestColumn from an
earlier scoring call. The first ensemble member propagated the latch to
g_ensembleChartMiReportDone and silenced every member on the chart.
The flag now latches only once a measurement exists. A short sample is
reported as a deferral naming the two numbers that identify it (cached bars
vs bars carrying a resolved label) and retried, up to
MI_REPORT_MAX_ATTEMPTS.
Build tag -> fleet-pool-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
bool fleetPin = false ;
if ( altPin = = " " )
{
//--- FRESH MODEL: pin the FLEET set, not this symbol's file header. Letting the file decide
//--- is what split the fleet into three incompatible training pools and orphaned SP500 -
//--- see ALTDATA_FLEET_COLUMNS for the full reasoning and the cost.
altPin = ALTDATA_FLEET_COLUMNS ;
fleetPin = true ;
}
m_altData . SetPinnedNames ( altPin ) ;
2026-08-16 15:12:54 -04:00
m_altData . Load ( m_symbol . Name ( ) , ( ENUM_TIMEFRAMES ) m_period ) ; // logs its own outcome; absence is normal
feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.
1. SP500 was training alone, and one alt-data column was the reason.
The alt block's width joins the model fingerprint, and the pool reader only
adopts peer rows whose fingerprint and width match. The exporter gives each
instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
13 - so the fleet ran as three incompatible pools:
EURUSD/USDJPY/USDCAD adopt ~57-60k peer rows each
XAUUSD/XTIUSD adopt 6.4k / 20.3k
SP500 "EVERY peer file was REJECTED, so this chart is
training alone" - 0 rows
SP500 therefore trained on 2279 independent observations against a 600-wide
input with its first layer floored at 16, printing its own "expect
overfitting" warning. It is the one chart with no pool and the worst
capacity ratio in the fleet by a factor of three.
Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
instead of their own file header. An existing model still adopts its .cfg
pin, so this re-keys nothing that is already trained.
Intersection rather than union: filling an absent series with its median
makes that column constant per instrument, which lets a pooled model
identify the source instrument and stop learning the shared mechanism. It
is also 6 columns narrower. Cost is six columns whose retained information
is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
to names.
2. The MI keep-screen disabled itself for the whole run on any cold start.
ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
the label cache is allocated before it is filled, so BuildMiSample finds no
row carrying a resolved label and returns 0 - a sixth exit, and the only
one the 8c1266d instrumentation did not cover, which is why it printed
nothing. observed then stayed -1, the permutation loop never iterated, and
the report emitted "-1.00000 nats over 0 permutations" beside a plausible
"strongest single feature 0.05979" that was a STALE m_miBestColumn from an
earlier scoring call. The first ensemble member propagated the latch to
g_ensembleChartMiReportDone and silenced every member on the chart.
The flag now latches only once a measurement exists. A short sample is
reported as a deferral naming the two numbers that identify it (cached bars
vs bars carrying a resolved label) and retried, up to
MI_REPORT_MAX_ATTEMPTS.
Build tag -> fleet-pool-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
m_altDataNamesPinned = altPin ; // stamped into the .cfg on the first save
if ( fleetPin )
Print ( ID + " : alt-data pinned to the FLEET column set ( " +
IntegerToString ( m_altData . FeatureCount ( ) ) + " columns) rather than this symbol's file "
" header. Every chart therefore publishes the same feature layout and can pool with "
" every other; a per-symbol set is what left SP500 training alone. " ) ;
2026-08-16 15:12:54 -04:00
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 = " " ;
}
2026-08-01 11:27:28 -04:00
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