Warrior_EA/Expert/Topology/Topology.mqh

785 lines
45 KiB
MQL5
Raw Permalink Normal View History

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
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| The fingerprint, the derived shape (width/taper/depth/conv |
//| filters/LSTM hidden), the conv/LSTM/batch-norm stages and |
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//| BuildFreshTopology. Owns exactly one piece of state - the |
//| measured swing geometry (see MeasureSwingGeometry); every other |
//| field these methods touch is shared elsewhere in the signal. |
//| InitNeuralNetwork()/InitFeatureIndicators() are NOT |
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
//| here: they are the network BOOT SEQUENCE (file locking, tester- |
//| cache seeding, chart/persistence/online-learning orchestration),|
//| a different and far more entangled job than deriving a shape - |
//| they stay in Expert\AIBase\Topology.mqh, called through the same |
//| forwards this file's methods are now reached through. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_TOPOLOGY_TOPOLOGY_MQH
#define WARRIOR_TOPOLOGY_TOPOLOGY_MQH
class CTopology
{
private:
CTopologyView *m_view; // BORROWED - the signal owns the adapter, not the reverse
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- THE MEASURED SWING GEOMETRY. The input window and the capacity budget are two questions about
//--- the SAME thing - how long this instrument's swing legs are - so one ZigZag walk answers both,
//--- and neither can be measured against a definition the other does not share.
bool m_swingGeometryValid;
double m_swingLegMedian; // bars between consecutive pivots, median
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
//--- LABEL-OVERLAP SPAN: how many labelled bars share one underlying event, which is what every
//--- consumer divides by. Under the pivot-event target that is the tolerance window, NOT the old
//--- "bars until the pivot PAIR commits" - see MeasureSwingGeometry() for why the distinction
//--- moved the capacity budget by a factor of ~15.
double m_swingLifespan;
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
int m_swingLegCount;
void MeasureSwingGeometry(void);
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
public:
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
//--- The leg-median default is HISTORY_BARS_FALLBACK. The overlap default is the tolerance window
//--- itself, which is the TRUE value rather than a conservative stand-in: unlike the old target,
//--- this label's overlap is a property of the label definition and not of the instrument's
//--- geometry, so there is nothing to measure and nothing to be wrong about before measuring.
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
CTopology(void) : m_view(NULL), m_swingGeometryValid(false),
m_swingLegMedian(HISTORY_BARS_FALLBACK),
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
m_swingLifespan(PIVOT_LABEL_TOLERANCE_BARS),
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingLegCount(0) { }
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
void Bind(CTopologyView *view) { m_view = view; }
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- Forces the next DeriveHistoryBars() to walk the chart again. The panel's weights reset is the
//--- only caller: it is what makes the "let history finish downloading, then reset" advice in the
//--- warnings below actually re-size the model instead of re-pinning the starved estimate.
void RemeasureSwingGeometry(void) { m_swingGeometryValid = false; }
double SwingLifespanEstimate(void) const { return MathMax(1.0, m_swingLifespan); }
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
string BuildModelFingerprint(void);
double EstimatedInSampleBarsRaw(void) const;
double EstimatedInSampleBars(void) const;
int FirstLayerFanIn(void) const;
int DeriveHistoryBars(void);
int ComputeHiddenLayerCount(void) const;
int ComputeConvFilterCount(void) const;
string FrontEndConfigSummary(void) const;
int LstmFanIn(void) const;
int ComputeLstmHiddenSize(void) const;
int ComputeFirstLayerWidth(void) const;
bool AddBatchNormStage(CArrayObj *topology, int units);
bool AddConvStage(CArrayObj *topology);
int ConvReceptiveFieldBars(void) const;
int ConvFirstStagePositions(void) const;
bool HasSecondConvStage(void) const;
int ConvOutputPositions(void) const;
int ConvOutputWidth(void) const;
bool AddLstmStage(CArrayObj *topology);
bool BuildFreshTopology(void);
};
//+------------------------------------------------------------------+
//| 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 CTopology::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_view.OptimizationAlgo(), LEGACY_HISTORY_BARS_SLOT, m_view.OutputNeuronsCount(),
m_view.NeuronsCount(), m_view.MinTrainYear(), LEGACY_CONVERGE_WR_SLOT, m_view.FractalPeriods(),
feat(target): delete the barrier/geometry stack - the label is the verdict Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE target and the era verdict is precision + recall per class against the label's own base rate - no win rate, no break-even, no expectancy, no geometry anywhere in training. DELETED - Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep, FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives in Labeling/LabelOverlap.mqh), 3 test EAs. - TripleBarrierLabel + walk, fractal label, geometry derivation/scan/ adoption, exit-policy replay, excursion MI targets, the drift verdict (DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry, the barrier defines, the .cfg geometry adopt (slots kept as zeros for the positional layout), the derived-geometry live-order override. - TRAINING_TARGET input/enum: direction models are always swing; META2 re-keys the meta head onto label agreement (descriptor loses its two geometry slots). REWORKED - Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is never cached, so it can never freeze as a false Neutral; training, calibration, OOS scoring and online learning all skip unresolved bars. - SDeployVerdict: significance-only; SOosTally chance = larger directional class share; pooled gate poolability = timeframe (record v2). - Purge/embargo/declustering gaps: the measured mean label resolution lag (LabelResolutionBars), not a barrier horizon. - Pool purge key + backfill DB rows: marked at the bar the label resolved on (m_labelResolveAge), not a fabricated barrier touch. - Online learning frontier: finality, not a horizon delay. - m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced -> m_erasSinceBest, ensemble vote outcome arrays -> label arrays. STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection are inputs again - trade management is the tester GA's search space. Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional, CUT token gone); META1 -> META2. Full retrain, as planned. Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs, 0 errors, 0 warnings each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- LEGACY SLOTS (were MinDirectionalRecallPct - the removed recall floor,
//--- shipped 40 - and m_focalGamma, removed 2026-07-31, which always
//--- contributed 0). Writing the same literals keeps every existing model's
//--- filename intact.
40, 0, m_view.OosSplitPct(), m_view.SwingConfirmationBars(),
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
(int)m_view.UseVolumes(), (int)m_view.UseTime(), (int)m_view.UseATR(), (int)m_view.UseSwingContext(),
(int)m_view.UseNews(), m_view.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);
ditch(features): remove the eight dead feature groups from the input matrix RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta, ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure, WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a closed verdict: the three oscillators are the same patterns that measured at chance as entries, and the Wyckoff family returned zero out-of-sample on five independent instruments - which is what closed the context score. RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks larger than it is. Every removed group contributed `flag ? N : 0` to the input width, and every flag was false, so the width was ALREADY zero for all eight: no .nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were hashed unconditionally and become literal 0 legacy slots (the convention the m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only when enabled, so their segments simply never appear - byte-identical to every fingerprint ever produced, since neither ever shipped on. CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted inside every .nnw, and Unflatten() rejects a size mismatch by falling back to constructor defaults - so dropping the dead fields would silently revert the tuned MA period of every model on disk while keeping its trained weights. That is the feature/weight mismatch this project has already paid for twice, and it is not worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing searches them. The class comment says all of this at the declaration. Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one indicator now, and a name saying "AD" for the MA handle is the kind of stale label that gets believed later. Its release-AFTER-recreate ordering is untouched - that is a documented fix, not bookkeeping. Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0 baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:21:03 -04:00
//--- LEGACY FEATURE SLOTS (were UseRSI and the five AD/Wyckoff flags, removed 2026-08-24). All
//--- six were hashed UNCONDITIONALLY and every one shipped false, so the literal zeros below are
//--- exactly what every .nnw on disk is already named after. Same rule as the m_focalGamma slot
//--- above: write the literal, keep the filename.
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
fp += StringFormat("|%d|%d|%d|%d|%d|%d|%d|%d",
ditch(features): remove the eight dead feature groups from the input matrix RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta, ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure, WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a closed verdict: the three oscillators are the same patterns that measured at chance as entries, and the Wyckoff family returned zero out-of-sample on five independent instruments - which is what closed the context score. RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks larger than it is. Every removed group contributed `flag ? N : 0` to the input width, and every flag was false, so the width was ALREADY zero for all eight: no .nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were hashed unconditionally and become literal 0 legacy slots (the convention the m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only when enabled, so their segments simply never appear - byte-identical to every fingerprint ever produced, since neither ever shipped on. CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted inside every .nnw, and Unflatten() rejects a size mismatch by falling back to constructor defaults - so dropping the dead fields would silently revert the tuned MA period of every model on disk while keeping its trained weights. That is the feature/weight mismatch this project has already paid for twice, and it is not worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing searches them. The class comment says all of this at the declaration. Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one indicator now, and a name saying "AD" for the MA handle is the kind of stale label that gets believed later. Its release-AFTER-recreate ordering is untouched - that is a documented fix, not bookkeeping. Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0 baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:21:03 -04:00
(int)m_view.UseMA(), 0,
0, 0,
0, 0,
0,
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
//--- 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);
//--- 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_view.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_view.SymbolName(), SYMBOL_CURRENCY_BASE) ==
SymbolInfoString(m_view.SymbolName(), 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_view.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_view.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. LEGACY SLOT: tau is fixed at 1.0 since the
//--- LogitAdjustTau input was removed; "100" keeps every model trained at the shipped default
//--- byte-identical.
fp += "|LA:100:BS";
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-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_view.UseVolumes())
fp += StringFormat("|VOL:%d", (int)VolumeData);
if(m_view.UseMA())
fp += StringFormat("|MAP:%d", (int)PeriodMA);
//--- 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";
feat(target): delete the barrier/geometry stack - the label is the verdict Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE target and the era verdict is precision + recall per class against the label's own base rate - no win rate, no break-even, no expectancy, no geometry anywhere in training. DELETED - Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep, FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives in Labeling/LabelOverlap.mqh), 3 test EAs. - TripleBarrierLabel + walk, fractal label, geometry derivation/scan/ adoption, exit-policy replay, excursion MI targets, the drift verdict (DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry, the barrier defines, the .cfg geometry adopt (slots kept as zeros for the positional layout), the derived-geometry live-order override. - TRAINING_TARGET input/enum: direction models are always swing; META2 re-keys the meta head onto label agreement (descriptor loses its two geometry slots). REWORKED - Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is never cached, so it can never freeze as a false Neutral; training, calibration, OOS scoring and online learning all skip unresolved bars. - SDeployVerdict: significance-only; SOosTally chance = larger directional class share; pooled gate poolability = timeframe (record v2). - Purge/embargo/declustering gaps: the measured mean label resolution lag (LabelResolutionBars), not a barrier horizon. - Pool purge key + backfill DB rows: marked at the bar the label resolved on (m_labelResolveAge), not a fabricated barrier touch. - Online learning frontier: finality, not a horizon delay. - m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced -> m_erasSinceBest, ensemble vote outcome arrays -> label arrays. STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection are inputs again - trade management is the tester GA's search space. Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional, CUT token gone); META1 -> META2. Full retrain, as planned. Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs, 0 errors, 0 warnings each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- TRAINING TARGET. The token covers the label's meaning - bump the number if any of it
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL ~2,300 lines. META had real, repeatedly measured ranking skill and ZERO operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4 pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the dose-response showed the high-conviction tail is temporally unstable - the precision-vs-threshold slope flips sign between calib and test on 3 of 4 symbols, so no ex-ante threshold rule exists. It shipped default-off and never gated a live entry. The self-measured tier weights are what actually rank the vote, and all six H4 instruments converged on them alone. RETRAIN-NEUTRAL, and that is the property that made this safe: - The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an if/else. Every direction model already took the SWG1 arm, so collapsing it to an unconditional append is byte-identical. No .nnw or .cfg is orphaned or re-keyed. - NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth() returned 0 for every direction model, so the input layer is unchanged. - DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs off AND meta on - a config that never shipped. Every existing .db keeps its filename. Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore, MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md. Unwound in place, the delicate part: Training.mqh carried four IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1 queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each wrapper is removed and the direction body promoted back to its original nesting - the bodies were never re-indented when the wrappers were added, so the promoted code is byte-identical to what ran before META existed. Also gone: the ensemble verdict's meta-veto replay and its approved/vetoed/unscored counters, the per-family/per-side OOS decomposition arrays, the m_isTrainQueueCand parallel queue and its lockstep shuffle, and the S2 era report. Also removed: the CMetaGate abstraction and the live CheckOpenPosition veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal() (META was the only non-voting child, so m_gates was always empty); m_parentSignal/SetParentSignal (existed only to reach the root's gate); SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep); IsMetaTarget() from all four view interfaces and their adapters; Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget. EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay, not just the corpus sweep; only its comment changed. The 2-output softmax arm in NetForward.mqh is kept too: it costs nothing and is the reusable binary-head path, now commented as unclaimed rather than as META's. Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the pre-edit baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
//--- changes. Emitted unconditionally: this used to branch to "|TGT:META2" for the meta head,
//--- and every direction model already took this arm, so collapsing it is byte-identical and
//--- re-keys nothing on disk.
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
//---
//--- SWG1 -> PVT1 (2026-08-25): the target changed from "which way is the next pivot" to "is a
//--- pivot about to commit, and which way does it turn" (SwingPivotDirectionLabel). Every .nnw on
//--- disk was fitted to the old meaning, so this MUST re-key or a resume would silently continue
//--- training weights against a target they were never fitted to - the class balance alone moves
//--- from ~56/44/0 to ~12/12/75. The tolerance window is in the token because it IS part of the
//--- label: widening it relabels every bar near a turn.
fp += StringFormat("|TGT:PVT1:%d", (int)PIVOT_LABEL_TOLERANCE_BARS);
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
//--- 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_view.IsEnsembleMember())
fp += "|ENS1";
feat(target): delete the barrier/geometry stack - the label is the verdict Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE target and the era verdict is precision + recall per class against the label's own base rate - no win rate, no break-even, no expectancy, no geometry anywhere in training. DELETED - Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep, FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives in Labeling/LabelOverlap.mqh), 3 test EAs. - TripleBarrierLabel + walk, fractal label, geometry derivation/scan/ adoption, exit-policy replay, excursion MI targets, the drift verdict (DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry, the barrier defines, the .cfg geometry adopt (slots kept as zeros for the positional layout), the derived-geometry live-order override. - TRAINING_TARGET input/enum: direction models are always swing; META2 re-keys the meta head onto label agreement (descriptor loses its two geometry slots). REWORKED - Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is never cached, so it can never freeze as a false Neutral; training, calibration, OOS scoring and online learning all skip unresolved bars. - SDeployVerdict: significance-only; SOosTally chance = larger directional class share; pooled gate poolability = timeframe (record v2). - Purge/embargo/declustering gaps: the measured mean label resolution lag (LabelResolutionBars), not a barrier horizon. - Pool purge key + backfill DB rows: marked at the bar the label resolved on (m_labelResolveAge), not a fabricated barrier touch. - Online learning frontier: finality, not a horizon delay. - m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced -> m_erasSinceBest, ensemble vote outcome arrays -> label arrays. STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection are inputs again - trade management is the tester GA's search space. Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional, CUT token gone); META1 -> META2. Full retrain, as planned. Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs, 0 errors, 0 warnings each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- No close-all token: the swing label aims at a pivot and owes nothing to the trade schedule.
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
return fp;
}
//+------------------------------------------------------------------+
//| Shared training-set size estimate - see the declaration comment. |
//+------------------------------------------------------------------+
double CTopology::EstimatedInSampleBarsRaw(void) const
{
int secs = PeriodSeconds(m_view.Timeframe());
if(secs <= 0)
secs = PeriodSeconds(PERIOD_H1);
double barsPerYear = (SECONDS_PER_YEAR / (double)secs) * MARKET_OPEN_FRACTION;
double oosKept = (100.0 - (double)m_view.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.
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
string sym = m_view.SymbolName();
datetime firstAvailableBar = (datetime)SeriesInfoInteger(sym, m_view.Timeframe(), SERIES_FIRSTDATE);
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
MqlDateTime floorTime;
TimeCurrent(floorTime);
floorTime.year = m_view.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;
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
int available = Bars(sym, m_view.Timeframe(), windowStart, TimeCurrent());
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
//--- 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)
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
Print(m_view.Id() + ": WARNING - only " + IntegerToString(available) + " bars of " + sym +
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
" 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 CTopology::EstimatedInSampleBars(void) const
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- DEFLATED BY LABEL OVERLAP (Lopez de Prado ch. 4): overlapping labels are not independent
//--- observations, and every budget below is stated per INDEPENDENT observation.
//---
//--- FROM THE MEASURED SWING GEOMETRY, not from m_labelOverlap. This runs inside
//--- InitNeuralNetwork, where the label cache does not exist yet (that same function sets
//--- m_labelCachePrebuilt = false a few lines later) - so the measured lifespan was still at its
//--- "nothing observed" default of 1.0 at every call, and the deflation this line performs could
//--- never once have fired. Every fresh model was therefore sized as though its labels did not
//--- overlap at all, over-budgeting the first dense layer by exactly this factor.
//---
//--- PLUS the pool: Use_Training_Pool feeds the trainer peer-chart rows this budget used to be
//--- completely blind to (see CExpertSignalAIBase::TopologyPooledIndependentBars() for the two
//--- conservative discounts applied). Recomputed every call, same as the own-chart term above -
//--- capacity that only exists for a BRAND-NEW build anyway (an already-trained model's actual
//--- layer widths come from its .nnw via LoadNetWithRetry(), never from this function).
return EstimatedInSampleBarsRaw() / SwingLifespanEstimate() + m_view.PooledIndependentBars();
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
}
//+------------------------------------------------------------------+
//| Fan-in of the first dense layer - see the declaration comment. |
//+------------------------------------------------------------------+
int CTopology::FirstLayerFanIn(void) const
{
int frontEndOut = m_view.UsesLstmStage() ? m_view.LstmHiddenSize()
: (m_view.UsesConvStage() ? ConvOutputWidth() : 0);
return (frontEndOut > 0) ? frontEndOut : (int)m_view.HistoryBars() * m_view.NeuronsCount();
}
//+------------------------------------------------------------------+
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//| THE SWING GEOMETRY OF THIS CONFIGURATION, measured once. |
//| |
//| Walks m_zigZag - THE LABEL'S OWN PIVOT SOURCE, not a lookalike. |
//| The window used to be measured with a private +/-12-bar fractal |
//| while SwingPivotDirectionLabel aimed at ZigZag(12,5,3) pivots, so |
//| the window was sized against a leg distribution the label never |
//| used, and the lifespan below could not have been derived from it |
//| at all. |
//| |
//| Reads the buffer through CopyBuffer on the indicator HANDLE |
//| rather than the CiCustom, for the same reason the OHLC reads here |
//| always used CopyHigh/CopyLow: this runs at InitNeuralNetwork, |
//| before ResizeBuffers() has sized anything to this depth. |
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
//+------------------------------------------------------------------+
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
void CTopology::MeasureSwingGeometry(void)
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
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingGeometryValid = false;
m_swingLegMedian = HISTORY_BARS_FALLBACK;
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
m_swingLifespan = PIVOT_LABEL_TOLERANCE_BARS;
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingLegCount = 0;
string sym = m_view.SymbolName();
int availableBars = Bars(sym, m_view.Timeframe());
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
int span = (int)MathMin(availableBars - 1, WINDOW_DERIVE_SPAN_BARS);
if(span < TOPOLOGY_BUDGET_MIN_TRUSTED_BARS)
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
Print(m_view.Id() + ": WARNING - only " + IntegerToString(availableBars) + " bars of " + sym +
" history are available yet, too few to measure the swing geometry from. Falling back to a " +
IntegerToString(HISTORY_BARS_FALLBACK) + "-bar window and the same figure for label overlap. "
"If this is a fresh install, let history finish downloading and then reset this model's "
"weights from the panel, which re-measures both.");
return;
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
}
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- newest CLOSED bars only (start 1): ZigZag's most recent leg is still being revised, and a
//--- repainting leg would enter the median as a length that never existed.
double zz[];
ArraySetAsSeries(zz, true);
if(CopyBuffer(m_view.ZigZagHandle(), 0, 1, span, zz) != span)
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
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
Print(m_view.Id() + ": WARNING - could not read " + IntegerToString(span) + " ZigZag values to measure "
"the swing geometry (the indicator is probably still calculating); falling back to a " +
IntegerToString(HISTORY_BARS_FALLBACK) + "-bar window and the same figure for label overlap. "
"Reset this model's weights from the panel once the chart has settled to re-measure both.");
return;
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
}
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- Bar distances between consecutive pivots, oldest -> newest. Pivots alternate by construction,
//--- so a non-zero buffer entry IS the next pivot and needs no high/low test.
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
double legs[];
ArrayResize(legs, 0, 256);
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
int prevPivot = -1;
for(int b = span - 1; b >= 0; b--)
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
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
if(zz[b] == 0.0 || !MathIsValidNumber(zz[b]))
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
continue;
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
if(prevPivot >= 0)
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
{
int n = ArraySize(legs);
ArrayResize(legs, n + 1, 256);
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
legs[n] = (double)(prevPivot - b); // series indices: newer bar = smaller index
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
}
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
prevPivot = b;
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
}
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingLegCount = ArraySize(legs);
if(m_swingLegCount < WINDOW_DERIVE_MIN_LEGS)
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
{
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
Print(m_view.Id() + ": WARNING - only " + IntegerToString(m_swingLegCount) + " ZigZag legs in " +
IntegerToString(span) + " bars, too few to trust a median. Falling back to a " +
IntegerToString(HISTORY_BARS_FALLBACK) + "-bar window and the same figure for label overlap.");
return;
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
}
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingLegMedian = MathMedian(legs);
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
//--- LABEL OVERLAP IS NO LONGER A PROPERTY OF THE SWING GEOMETRY (2026-08-25). It used to be:
//--- the old direction-to-next-pivot target gave every bar of a leg the same answer, so a bar
//--- d bars before pivot P waited d + (the leg leaving P) bars to resolve, and the mean of that
//--- over the measured legs - about 31 bars here - was the right deflator.
//---
//--- The pivot-event target overlaps only over its TOLERANCE WINDOW: one turn can be called by
//--- the PIVOT_LABEL_TOLERANCE_BARS bars in front of it and by no others, whatever the legs
//--- around it look like. Consecutive windows share all but one bar, so mean uniqueness is 1/T
//--- and the deflator is T - a constant of the label, not a measurement of the instrument.
//---
//--- THIS IS A ~15x CHANGE IN THE CAPACITY BUDGET and it is the intended consequence, not a
//--- side effect: EstimatedInSampleBars() divides by this, so every model was being sized for
//--- ~368-1086 independent observations when the new label supplies ~5,700-17,600. That is what
//--- lifts the first dense layer off its FIRST_LAYER_MIN_WIDTH floor, and with it the derived
//--- hidden-layer count off MIN_HIDDEN_LAYERS. The legs are still walked above: m_swingLegMedian
//--- still sets the input window, and the leg count still gates the too-few-legs warning.
m_swingLifespan = MathMax(1.0, (double)PIVOT_LABEL_TOLERANCE_BARS);
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_swingGeometryValid = true;
}
//+------------------------------------------------------------------+
//| Derived input-window length - see the declaration comment. |
//+------------------------------------------------------------------+
int CTopology::DeriveHistoryBars(void)
{
if(!m_swingGeometryValid)
MeasureSwingGeometry();
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
//--- 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++)
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
if(ladder[i] <= m_swingLegMedian)
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
window = ladder[i];
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
PrintFormat("%s: derived swing geometry - input window %d bars (median ZigZag leg %.1f over %d legs,"
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
" snapped down to the ladder%s), label overlap %.1f bars. Measured once at model"
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
" creation and pinned in the .cfg; an existing model adopts its own trained window"
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
" instead. The overlap is what the capacity budget divides by - see"
" ComputeFirstLayerWidth. It is now the pivot label's tolerance window, a constant of"
" the target, not a measurement of these legs - see MeasureSwingGeometry.",
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
m_view.Id(), window, m_swingLegMedian, m_swingLegCount,
m_swingLegMedian > 32 ? ", CAPPED at 32 - era time scales with the window" : "",
SwingLifespanEstimate());
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
return window;
}
//+------------------------------------------------------------------+
//| Dense-taper depth - see the declaration comment. |
//+------------------------------------------------------------------+
int CTopology::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_view.OutputNeuronsCount(), HIDDEN_TAPER_MIN_WIDTH);
lastHidden = (int)MathMin(lastHidden, m_view.InitialNeuronsCount());
if(lastHidden <= 0 || m_view.InitialNeuronsCount() <= lastHidden)
return MIN_HIDDEN_LAYERS;
double steps = MathLog((double)m_view.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 CTopology::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_view.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 CTopology::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(m_view.UsesConvStage())
{
s += " | conv " + IntegerToString(ConvReceptiveFieldBars()) + " bars x" +
IntegerToString(m_view.NeuronsCount()) + "->" + IntegerToString(m_view.ConvFilterCount()) +
" (" + IntegerToString(ConvFirstStagePositions()) + " pos)";
if(HasSecondConvStage())
s += " | pool /" + IntegerToString(m_view.ConvFilterCount()) +
" | conv2 ->" + IntegerToString(ConvOutputPositions()) + " pos x" +
IntegerToString(m_view.ConvFilterCount()) + " = " + IntegerToString(ConvOutputWidth());
else
s += " = " + IntegerToString(ConvOutputWidth());
}
if(m_view.UsesLstmStage())
s += " | lstm " + IntegerToString(LstmFanIn()) + "->" + IntegerToString(m_view.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 = m_view.UsesLstmStage() ? m_view.LstmHiddenSize()
: (m_view.UsesConvStage() ? ConvOutputWidth() : 0);
if(frontEndOut > 0 && m_view.InitialNeuronsCount() > frontEndOut)
s += " | NOTE dense fans out " + IntegerToString(frontEndOut) + "->" +
IntegerToString(m_view.InitialNeuronsCount());
return s;
}
//+------------------------------------------------------------------+
//| Input width the LSTM block actually receives. |
//+------------------------------------------------------------------+
int CTopology::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(m_view.HasConvBeforeLstm())
return ConvOutputWidth();
return (int)m_view.HistoryBars() * m_view.NeuronsCount();
}
//+------------------------------------------------------------------+
//| LSTM recurrent hidden width - see the declaration comment. |
//+------------------------------------------------------------------+
int CTopology::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 ? (m_view.HasConvBeforeLstm() ? m_view.ConvFilterCount() : m_view.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 CTopology::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 = m_view.UsesLstmStage() ? m_view.LstmHiddenSize()
: (m_view.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.
feat(label): pivot-EVENT target replaces direction-to-next-pivot The old target asked "which way is the next pivot", which every bar of a ~13-20 bar leg answers identically - so the net could not tell a fresh turn from mid-trend and learned the prevailing direction instead. Its own zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the drift, and the gate's standing warning ("a model that only reproduces it has found the drift, not an edge") applied to the target itself. Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars, Sell a swing HIGH, Neutral no turn that close. Pivot type is read from ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE verdict, so the Neutral majority is permanent rather than provisional. Measured on a full fresh run, all 6 charts: class balance 56/44/~0 -> 13.7/13.7/72.6 (imbalance 5.3:1) label overlap ~31 bars -> 5 bars independent obs 368-1086 -> 2331-7032 weights/obs 9.2-26.2 -> 1.1-4.2 coverage 100% of bars -> 17-48% 23 of 24 models fire all three classes at precision 18-32% vs 13-15% chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar). Two bindings had to move with the label: - The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as raw/31, measured from the legs. Overlap is now a property of the LABEL - one turn is callable by exactly the tolerance window - so it is the window, not a leg measurement. Missing this would have kept every model sized for a sixth of its real evidence. - A dormant cold-start seed. Labels.mqh seeds the output bias toward the dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a true prior spread of ~1.75, which would start every net predicting Neutral ~95% of the time. Now seeds the measured log-prior, zero-centred and capped by the same guard rail the logit adjustment uses (Lin et al. 2017). TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is part of the label: every .nnw is invalidated and the fleet retrains. Depth is still gated, and now for a precise reason: the first dense layer stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2 at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature pruning - not architecture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
string basis = StringFormat("%.0f independent in-sample observations (%.0f bars / %.1f-bar label"
" overlap)", isBars, EstimatedInSampleBarsRaw(),
fix(topology): size the network against observations, not bars The capacity budget is stated in weights per INDEPENDENT observation and divides by the mean label lifespan to get there. It never once did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is only ever called from InitNeuralNetwork, where the label cache does not exist yet (that same function sets m_labelCachePrebuilt = false a few lines below), so MeanLifespan() returned its "nothing measured" default of 1.0 at every call. Every fresh model was sized as though its labels did not overlap - over-budgeting the first dense layer by a factor of L, which is several rungs of a power-of-two ladder. The "expect overfitting, reduce the feature set or pool instruments" warning is the branch that should fire on H1 and structurally could not. Fixed at the source rather than by reordering the boot sequence (the prebuild is chunked across Train() calls and cannot complete inside init): MeasureSwingGeometry() walks the ZigZag ONCE at init and answers both questions from it - the median leg gives the window, and the leg series gives the mean label lifespan analytically. SwingPivotDirectionLabel resolves bar i when the SECOND pivot after it commits, so a bar d bars before pivot P waits d + (the leg leaving P); summed over every bar of every leg that is exactly the mean the label walk accumulates. That also closes the coherence gap the swing target opened: the window was measured with a private +/-12-bar fractal while the label aimed at ZigZag(12,5,3) pivots, so it was sized against a leg distribution the label never used. One pivot source now, the label's. Also: - ResetWeights() re-derives the shape. It rebuilt from the members a history-starved init had pinned and re-saved them - so the "let history download, then reset from the panel" advice in both fallback warnings did nothing at all. - The CAPACITY line prints the measured lifespan beside the one the topology was sized for, and warns when they differ by more than a ladder rung. That is the check that makes the estimator falsifiable. - Topology reads the view's symbol, not _Symbol (latent for pooling). - Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0: under-sizing is recoverable, over-sizing silently is not. Compile: 0 errors, 0 warnings (stage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
SwingLifespanEstimate());
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
if(budget < FIRST_LAYER_MIN_WIDTH)
Print(m_view.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 CTopology::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_view.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 CTopology::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_view.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_view.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_view.NeuronsCount();
desc.step = m_view.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 CTopology::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_view.HistoryBars());
return (bars > 0 ? bars : 1);
}
//+------------------------------------------------------------------+
int CTopology::ConvFirstStagePositions(void) const
{
//--- Sliding positions of stage 1: window ConvReceptiveFieldBars() bars, step 1 bar.
int p = (int)m_view.HistoryBars() - (ConvReceptiveFieldBars() - 1);
return (p > 0 ? p : 1);
}
//+------------------------------------------------------------------+
bool CTopology::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 CTopology::ConvOutputPositions(void) const
{
int p = ConvFirstStagePositions();
return (HasSecondConvStage() ? p - (ConvReceptiveFieldBars() - 1) : p);
}
//+------------------------------------------------------------------+
int CTopology::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_view.ConvFilterCount();
}
//+------------------------------------------------------------------+
//| LSTM sequence stage. Shared by CSignalLSTM and CSignalHYBRID - |
//| see the declaration comment. |
//+------------------------------------------------------------------+
bool CTopology::AddLstmStage(CArrayObj *topology)
{
if(CheckPointer(topology) == POINTER_INVALID)
return false;
CLayerDescription *desc = new CLayerDescription();
if(CheckPointer(desc) == POINTER_INVALID)
return false;
desc.count = m_view.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_view.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 ? (m_view.HasConvBeforeLstm() ? m_view.ConvFilterCount() : m_view.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_view.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 CTopology::BuildFreshTopology(void)
{
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;
}
desc.count = m_view.NetInputWidth();
desc.type = defNeuron;
desc.activation = NONE;
desc.optimization = (ENUM_OPTIMIZATION)m_view.OptimizationAlgo();
if(!Topology.Add(desc))
{
delete Topology;
return false;
}
//--- neuron-type-specific layers (Conv+Pool, LSTM, or none for a plain perceptron)
if(!m_view.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_view.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_view.InitialNeuronsCount());
double taperRatio = (m_view.HiddenLayersCount() > 1)
? MathPow((double)lastHidden / (double)m_view.InitialNeuronsCount(), 1.0 / (double)(m_view.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_view.HistoryBars() * m_view.NeuronsCount());
bool result = true;
for(int i = 0; (i < m_view.HiddenLayersCount() && result); i++)
{
int n = (i == 0)
? m_view.InitialNeuronsCount()
: MathMax(lastHidden, (int)MathRound(m_view.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 = m_view.HiddenLayerActivation();
desc.optimization = (ENUM_OPTIMIZATION)m_view.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_view.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 = m_view.OutputLayerActivation();
desc.optimization = (ENUM_OPTIMIZATION)m_view.OptimizationAlgo();
if(!Topology.Add(desc))
{
delete Topology;
return false;
}
//--- The whole point of consolidating this into one view call: deleting the OLD net, constructing
//--- the new one and reporting validity is irreducible pointer/object work, not signal state - see
//--- ITopologyView.mqh's comment.
bool netOk = m_view.ReplaceNetFromTopology(Topology);
delete Topology;
if(!netOk)
return false;
//--- A fresh topology invalidates any existing shadow and the whole online-learning history (a
//--- brand-new untrained net has none) - see COnlineLearning::ResetForFreshTopology()'s comment.
m_view.ResetOnlineLearningForFreshTopology();
return true;
}
#endif // WARRIOR_TOPOLOGY_TOPOLOGY_MQH
//+------------------------------------------------------------------+