2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//+------------------------------------------------------------------+
2026-08-01 11:27:28 -04:00
//| Topology.mqh |
//| |
//| Network bootstrap and topology construction: the derived shape |
//| (width/taper/depth/conv filters/LSTM hidden), the conv, LSTM and |
//| batch-norm stages, BuildFreshTopology and InitIndicators. |
//+------------------------------------------------------------------+
# ifndef WARRIOR_AIBASE_TOPOLOGY_MQH
# define WARRIOR_AIBASE_TOPOLOGY_MQH
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| 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. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
2026-08-19 23:50:30 -04:00
string CExpertSignalAIBase : : BuildModelFingerprint ( void )
2026-08-01 11:27:28 -04:00
{
//--- 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
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
string fp = StringFormat ( " %d|%d|%d|%d|%d|%d|%.2f|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d " ,
2026-08-11 21:53:37 -04:00
//--- LEGACY_HISTORY_BARS_SLOT: the window left this hash 2026-08-11 when
//--- it became DERIVED - same rule and reason as every derived field above;
//--- keyed on a measured quantity, the filename would change the moment more
//--- history downloads. The .cfg is the record (adopt-don't-compare).
m_optimizationAlgo , LEGACY_HISTORY_BARS_SLOT , m_outputNeuronsCount ,
2026-08-01 11:27:28 -04:00
m_neuronsCount , m_minTrainYear , LEGACY_CONVERGE_WR_SLOT , m_fractalPeriods ,
//--- LEGACY SLOT (was m_focalGamma, removed 2026-07-31). It was a double fed
//--- to a %d conversion, so it always contributed the literal below rather
//--- than the configured gamma - the shipped fingerprints read ...|40|0|30|...
//--- Writing the same literal keeps every existing model's filename intact.
m_minDirectionalRecallPct , 0 , m_oosSplitPct , m_swingConfirmationBars ,
( int ) m_useVolumes , ( int ) m_useTime , ( int ) m_useATR , ( int ) m_useSwingContext ,
( int ) m_useNews , m_newsFeatureWindowMinutes ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
if ( ForceHiddenLayers > 0 )
fp + = StringFormat ( " |FHL:%d " , ForceHiddenLayers ) ;
2026-08-22 00:25:52 -04:00
//--- THE TRIPLE-BARRIER SHAPE LEFT THIS HASH ON 2026-08-07, when SL_Mode/TP_Mode stopped being
//--- inputs and became MEASURED by ReportBarrierGeometryScan.
2026-08-01 11:27:28 -04:00
fp + = StringFormat ( " |%d|%d|%d|%d|%d|%d|%d|%d " ,
( int ) m_useMA , ( int ) m_useRSI ,
( int ) m_useADCumulativeDelta , ( int ) m_useADShorteningOfThrust ,
( int ) m_useADWyckoffEventStream , ( int ) m_useADWyckoffFailedStructure ,
( int ) m_useADWyckoffSignificantBarInversion ,
//--- starting MA TYPE (MA_Type input): changes the MA feature's values, so a change
//--- must invalidate the cache. The auto-tuned type/period themselves live in the
//--- .nnw indicator-param block (Flatten/Unflatten), not here - this is the seed only.
( int ) MA_Type ) ;
2026-08-22 00:25:52 -04:00
//--- MACD/Ichimoku feature flags, appended ONLY WHEN ENABLED rather than unconditionally like
//--- every flag above. Conditional append leaves those fingerprints byte-identical. Anything
//--- added here in future should follow the same rule.
2026-08-01 11:27:28 -04:00
if ( m_useMACD )
fp + = StringFormat ( " |MACD:%d:%d:%d " , ( int ) MACD_PeriodFast , ( int ) MACD_PeriodSlow , ( int ) MACD_PeriodSignal ) ;
if ( m_useIchimoku )
fp + = StringFormat ( " |ICHI:%d:%d:%d " , ( int ) Ichimoku_PeriodTenkan , ( int ) Ichimoku_PeriodKijun , ( int ) Ichimoku_PeriodSenkou ) ;
2026-08-22 00:25:52 -04:00
//--- 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
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
//--- time and pinned in the .cfg instead.
if ( m_useCrossAsset )
2026-08-11 21:07:52 -04:00
{
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
fp + = StringFormat ( " |XA:%d " , CROSSASSET_FEATURES ) ;
2026-08-22 00:25:52 -04:00
//--- INDEX-MODE RE-ENCODE (2026-08-11). Same width, different SEMANTICS - so models trained
//--- under the old degenerate encoding must re-key.
2026-08-11 21:07:52 -04:00
if ( SymbolInfoString ( m_symbol . Name ( ) , SYMBOL_CURRENCY_BASE ) = =
SymbolInfoString ( m_symbol . Name ( ) , SYMBOL_CURRENCY_PROFIT ) )
fp + = " :IDX2 " ;
}
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
//--- Spread feature: conditional append, same rule. Nothing measured goes in - the spread series
//--- itself is market data, not configuration.
if ( m_useSpreadFeature )
fp + = " |SPR:2 " ;
2026-08-22 00:25:52 -04:00
//--- 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).
perf(features): the external block enters the window once, not once per bar
Measured on the live SP500 D1 export (6073 rows, 13 features, 5888 simulated
16-bar windows):
distinct values per feature per window : 1.7 - 2.7 of 16 slots
variance in the first 13 PCs : 96.5 - 97.0%
components for 95% / 99% : 12 / 17-19
effective rank (entropy) : ~11.5
208 inputs carrying about 12 dimensions. Only 6 of the 13 features move daily
(VIX complex, USD, the rates trio); 5 are weekly (COT, EIA, output gap) and 2
monthly (CPI, unemployment). The lookup is as-of by bar open time into a DAILY
file, so bars sharing a calendar day are byte-identical by construction.
The cost is NOT overfitting capacity - collinear copies span ~12 directions,
not 208, so an earlier claim that this wasted 26% of the model overstated it.
It is GRADIENT WEIGHTING. Batch norm standardizes each of the 208 coordinates
independently; that rescales the copies without decorrelating them, so one
factor arrives on 16 unit-variance coordinates, each weight takes a full-size
step, and the factor's aggregate coefficient moves ~16x faster than a per-bar
price feature's. The network was biased toward the external block by a factor
of the window length - and pointing the wrong way, since these features cleared
only a marginal incremental screen while price is the base signal.
Zeroed at WINDOW ASSEMBLY, not in BufferTempData: that output is cached PER BAR
and a bar sits at slot 15 of one window and slot 0 of the next, so a
slot-dependent value there would poison the cache or force a recompute per slot.
The cache keeps true values; only this window's copies are cleared. Width
contract untouched - same count, same positions - so conv/LSTM/HYBRID keep their
bar-major rectangle unchanged and the block arrives at the newest bar, which for
the LSTM is the final timestep. Zero-variance coordinates are safe through batch
norm (divisor is MathMax(MathSqrt(var + BN_EPSILON), BN_MIN_STD)).
Fingerprint gains |ALTW:1 when alt data is on. Same width and same .cfg, so
nothing else would have caught a model trained under the replicated layout
resuming under this one. Conditional append per the existing rule: configs
without alt data keep their fingerprints and their trained models.
NOT the concat branch. CNet is a strictly linear stack (CLayerDescription has no
input-source field; NetBuild wires i to i+1 and stores layer L's weights on
L-1), so a real two-tower model needs a new multi-input layer type across
WarriorCPU, WarriorDML and the OpenCL kernels plus an .nnw format change - the
highest-risk change in this repo, in the code that produced the transposed dense
gradient, the Adam second-moment bug and the reversed LSTM window. This captures
the part of that idea the measurement actually supports, at no engine risk.
Compile-verified: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:05:10 -04:00
if ( m_useAltData )
fix(features): collapse only the anchor's own run - leave lagged readings put
User's call before deploy: "I would rather avoid lagging so the NN finds
accurate patterns." Correct instinct, and it picks the conservative variant.
110b384 deduplicated the WHOLE window, so every distinct reading survived at one
slot. The flaw is which slot: it depends on where the calendar-day boundary falls
inside that particular window, and on H4 that boundary cycles through ~6 phases.
A dense layer holds a separate weight per (slot, feature), so a given lag would
have landed on a different coordinate from one window to the next - turning a
stable lagged input into a moving one.
Now it blanks only bars carrying a BYTE-IDENTICAL copy of the anchor's reading
and stops at the first bar that differs. An as-of lookup into a daily file is a
step function in time, so those copies are exactly the contiguous run of bars
sharing the anchor's calendar day. Everything older keeps its natural replicated
run, in the same slots it always occupied - whatever the net learned to read
there, it still reads there.
Why the anchor's reading is the right one to isolate: the window's newest slot IS
the bar being predicted (BuildFeatureWindow's final iteration lands on r, and
pass 3 grades that same index), so it is the reading contemporaneous with the
decision - and the only one the alt screens ever validated. They measured the
CURRENT reading's MI against forward range and never tested lags, so the lagged
content is unproven, which is a reason to leave it undisturbed rather than a
licence to rearrange it.
What is still fixed: the anchor's reading reaches the first layer on one
coordinate instead of once per bar of its day, removing the ~16x gradient
upweight for the validated signal. And this is IDENTICAL to full dedup exactly
where replication was worst - on M15/H1 the whole window sits inside one calendar
day, so the anchor's run is the whole window - and a no-op on D1, where the bar
before the anchor is already a different day and the loop breaks immediately.
The two differ only on middle timeframes, and there this is the safe side.
Fingerprint |ALTW:1 -> |ALTW:2 so nothing trained under the hour-old full-dedup
semantics can silently resume under these.
Compile-verified: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:21:22 -04:00
fp + = " |ALTW:2 " ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
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
2026-08-22 00:25:52 -04:00
//--- without it. ":BS" = the correction spans Buy/Sell only, with Neutral (the abstain outcome)
//--- never subsidised - see ApplyLogitAdjustment.
2026-08-01 11:27:28 -04:00
if ( m_logitAdjustTau > 0.0 )
fix(imbalance): the class-imbalance correction was subsidising the abstain class
NOT COMPILED - user compiles.
Root cause of the Neutral collapse. Logit adjustment (Menon et al. 2020) makes a
classifier Bayes-optimal for BALANCED error by subsidising rare classes. It was
wired here when Neutral was the DOMINANT class - the "big move up / big move down
/ nothing much" era, where the correction pulled the model off the majority.
The triple-barrier relabel (b4a704d) inverted the distribution. The barriers are
now the EA's own SL/TP, so ~89% of bars RESOLVE and only timeouts are Neutral.
Measured on SP500 H4, from the EA's own log:
measured priors Buy 48.26% Sell 41.13% Neutral 10.61%
log-prior spread 1.52 | tau 1.00 CAPPED to 0.79
Neutral became the RAREST class, so the correction started subsidising it - by
tau*(log pB - log pN) = 1.20 logits. With no directional edge to overcome that
(direction is closed at best-of-999, p=1.0000), the model took the free lunch:
OOS recall Buy:1% Sell:0% Neutral:100%
OOS raw out spread avg 0.9993 (softmax saturated, near one-hot)
dW/W bn1 0.000% bn3 2.0% bn5 6.5% (input weight block frozen; head twitching)
The anti-collapse mechanism was the collapse. The recall gate needs >=40% on all
three classes, so nothing could ever deploy and the plateau ladder burned eras.
Present in both runs today (b6b5 froze bn1 by era ~719, 17ae by ~169), so it
predates this week's work.
FIX: the correction now spans the DECIDABLE classes only, Buy against Sell,
centred on their midpoint, with Neutral pinned at offset 0. Neutral is the
ABSTAIN outcome and abstention already has a better owner - m_dirConfThreshold,
refitted every era on the held-out calibration band against a coverage floor and
the measured break-even. Subsidising the abstain class does that job twice and
spends the whole correction suppressing the only decisions that can pay.
What still gets corrected is real: a trending symbol resolves more long barriers
than short, and uncorrected the model inherits that as a standing directional
bias. Here it is log(0.4826)-log(0.4113) = 0.16, so the offsets are tiny - the
correct answer, not a broken one. The two traded classes were already balanced;
the old spread of 1.52 only ever described how rare a timeout is.
Everything is derived from the measured distribution, as requested - offsets from
the priors, cap from the resulting spread. tau itself is deliberately NOT fitted:
tuning it against the same data that selects the checkpoint would add another
search dimension to a project that has been burned by exactly that. tau=1 is the
theory value and the cap (now ~9.5x looser at spread 0.16) will rarely bind.
Log line now reports both spreads and, when the abstain class is the rarest, says
how much the old form would have boosted it. Fingerprint |LA:<tau> -> |LA:<tau>:BS
so models trained under the all-three form re-key instead of resuming.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:43:30 -04:00
fp + = StringFormat ( " |LA:%d:BS " , ( int ) MathRound ( m_logitAdjustTau * 100.0 ) ) ;
2026-08-22 00:25:52 -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.
2026-08-01 11:27:28 -04:00
fp + = " |MR:1:90:1 " ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
if ( m_useVolumes )
fp + = StringFormat ( " |VOL:%d " , ( int ) VolumeData ) ;
if ( m_useMA )
fp + = StringFormat ( " |MAP:%d " , ( int ) PeriodMA ) ;
if ( m_useRSI )
fp + = StringFormat ( " |RSIP:%d " , ( int ) PeriodRSI ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
fix: the sequence models were reading the window backwards
BuildFeatureWindow() replaces eight hand-rolled copies of the same loop
and feeds the window OLDEST BAR FIRST. Every copy fed it newest-first,
because MQL5 timeseries indices run backwards and `r + b` with b ascending
walks into the past.
Harmless for PAI and CONV - a dense layer learns a weight per position
either way, a conv learns time-mirrored kernels. Not harmless for the
recurrent stacks:
- LSTM_SeqStepForward reads `inputs + t*Iw`, so step t is block t.
- It writes output[] only when t == steps-1: the visible output IS the
last hidden state.
- c_t = f*c_{t-1} + i*g decays toward the start of the sequence.
lstm_seq_flowcheck.cpp measured block 0's influence on the output at
1.2e-2 of block T-1's, at the shipped forget bias of 1.0.
So the bar being PREDICTED sat at the far end of the decay and the output
was handed to the OLDEST bar in the window - the exact inverse of what the
window is for. ~80x backwards on LSTM and HYBRID, on all three tiers
(OpenCL kernel, CPU DLL, pure-MQL5 inference), which is why it never
surfaced as a backend discrepancy.
This does not create edge - the MI diagnostics read at the noise floor
(p=0.4975) with a working positive control. It makes the one hypothesis
those diagnostics explicitly do NOT cover testable: they are marginal and
per-bar, and state they "cannot rule out one that only exists in
combination or across time". The sequence model is the instrument for
across-time structure and it has been crippled, so that hypothesis has
never been honestly tested.
Fingerprint gets an unconditional |WIN:2 - the vector keeps its shape and
its features, so a stale .nnw would load cleanly and run a model fitted to
one ordering against the other, silently. Re-keying every config is the
point, not collateral damage. FORCES A FULL RETRAIN.
Also: the now-relative bar caches are re-keyed on the two live paths.
EnsureBarCachesCapacity() was only ever called from training paths, but
once m_trainingComplete is set ScheduleTrainingIfNeeded() routes every bar
to RefreshConvergedSignal() and Train() is never re-entered - so nothing
cleared the feature cache again for the life of the process. A chart that
trained to convergence kept replaying the rows computed for the last
training era's bar grid: the live signal froze at its convergence-time
value, and OnlineLearnStep() backpropped those stale features against
freshly resolved labels. Backtests were never affected (an inference-only
process never allocates the arrays, so every read recomputes).
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:28:44 -04:00
fp + = " |WIN:2 " ;
2026-08-22 00:25:52 -04:00
//--- WYCKOFF CATEGORICAL ENCODING VERSION. Bump the number rather than adding a flag if the
//--- encoding is ever revisited.
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
if ( m_useADWyckoffEventStream )
fp + = " |WES:2 " ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- TRAINING TARGET (meta-labeling). Conditional, so every existing direction model keeps its
2026-08-22 00:25:52 -04:00
//--- byte-identical fingerprint. The 2-output head and the State\META\ folder already separate
//--- the FILES; this separates the SEMANTICS.
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if ( IsMetaTarget ( ) )
fp + = " |TGT:META1 " ;
2026-08-15 04:44:10 -04:00
//--- Same contract for the fractal-direction target (TrainingTarget input): the token covers the
2026-08-22 00:25:52 -04:00
//--- label's meaning (next confirmed 5-bar fractal extreme, min-move floor, tie handling) - bump
//--- the number if any of that changes.
2026-08-15 04:44:10 -04:00
if ( IsFractalTarget ( ) )
fp + = " |TGT:FRA1 " ;
2026-08-15 16:50:36 -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
2026-08-22 00:25:52 -04:00
//--- duplicate-chart guard exists precisely to stop concurrent writers).
2026-08-15 16:50:36 -04:00
if ( m_ensembleMember )
fp + = " |ENS1 " ;
2026-08-22 00:25:52 -04:00
//--- The scheduled close-all became part of the LABEL'S MEANING on 2026-08-19:
//--- TripleBarrierLabel stops its walk at the next scheduled flat, so the same chart with a
//--- Friday-23:45 schedule and with an everyday-22:00 schedule trains two DIFFERENT targets.
2026-08-19 11:23:54 -04:00
if ( ( int ) targetDayOfWeek ! = -1 & & ( int ) targetHour ! = -1 & & ( int ) targetMinutes ! = -1 )
fp + = " |CUT: " + IntegerToString ( ( int ) targetDayOfWeek ) + " @ " + IntegerToString ( ( int ) targetHour ) +
" : " + IntegerToString ( ( int ) targetMinutes ) ;
2026-08-19 23:50:30 -04:00
return fp ;
}
//+------------------------------------------------------------------+
//| Common network bootstrap shared by every AI signal: sets up |
//| indicators, then loads a saved network or builds a fresh one |
//| whose only per-signal-type difference is AddCustomLayers(). |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : InitNeuralNetwork ( CIndicators * indicators )
{
if ( m_isInitialized )
return true ;
if ( indicators = = NULL )
return false ;
m_indicatorsPtr = indicators ;
if ( ! CExpertSignalCustom : : InitIndicators ( indicators ) )
return false ;
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
if ( ! InitFeatureIndicators ( indicators ) )
2026-08-19 23:50:30 -04:00
return false ;
//--- Kick the terminal's async history sync for every cross-asset reference symbol NOW, at init,
//--- so the ~minute of cross-symbol download runs while the model loads and the label cache
//--- prebuilds - instead of starting only when the first Build() call finds the symbols unselected
//--- and the first era (and the one-shot MI report) runs with the panel absent. Non-blocking.
if ( m_useCrossAsset )
m_crossAsset . Warm ( ( ENUM_TIMEFRAMES ) m_period ) ;
Net = new CNet ( NULL ) ;
if ( CheckPointer ( Net ) = = POINTER_INVALID )
return false ;
2026-08-22 00:25:52 -04:00
//--- Size the first dense layer to the data. InitIndicators() above is what finalises
//--- m_neuronsCount, so this is the earliest point the input width is actually known. ORDER
//--- MATTERS, and it changed on 2026-08-09.
2026-08-19 23:50:30 -04:00
m_historyBars = DeriveHistoryBars ( ) ;
m_convFilterCount = ComputeConvFilterCount ( ) ;
m_lstmHiddenSize = ComputeLstmHiddenSize ( ) ;
m_initialNeuronsCount = ComputeFirstLayerWidth ( ) ;
//--- Depth LAST of the four: it is derived from the first-layer width above, so it cannot be settled
//--- before that one is. All four are overwritten from the .cfg further below if this configuration
//--- already has a trained model - see the adopt-don't-compare block there.
m_hiddenLayersCount = ComputeHiddenLayerCount ( ) ;
2026-08-22 00:25:52 -04:00
//--- The name used to carry a dense-depth tag ("Perceptron 3L"), from when AIType let a user
//--- pick MLP_3L vs MLP_4L and the depth was the only thing separating two charts of the same
//--- family.
2026-08-19 23:50:30 -04:00
string fp = BuildModelFingerprint ( ) ;
2026-08-01 11:27:28 -04:00
//--- FNV-1a 32-bit -> 8 hex chars: compact, deterministic, order-stable, collision-safe enough for
//--- the small optimizer grids in play (a collision would merely fail the .cfg guard and retrain).
uint fpHash = 2166136261 ;
int fpLen = StringLen ( fp ) ;
for ( int fpi = 0 ; fpi < fpLen ; fpi + + )
{
fpHash ^ = ( uint ) StringGetCharacter ( fp , fpi ) ;
fpHash * = 16777619 ;
}
m_fileName + = " _ " + DoubleToString ( MathRound ( m_outputNeuronsCount ) ) + " _ " + DoubleToString ( MathRound ( m_optimizationAlgo ) ) + " _ " + StringFormat ( " %08x " , fpHash ) ;
//--- Finish the display name with the model's short id and the leading 4 hex digits of that same
2026-08-22 00:25:52 -04:00
//--- fingerprint, so every log line and panel names the model file it belongs to. Their files
//--- were never at risk; the TAG was simply unable to do its one job.
2026-08-01 11:27:28 -04:00
string cfgTag = " [ " + m_id + " - " + StringSubstr ( StringFormat ( " %08x " , fpHash ) , 0 , 4 ) + " ] " ;
if ( StringFind ( ID , cfgTag ) < 0 )
ID + = cfgTag ;
2026-08-22 00:25:52 -04:00
//--- One self-verifying config line per chart, deliberately NOT gated on VerboseMode. A multi-
//--- chart comparison is only valid if every chart is identical except the axis under test, and
//--- until now a drifted setting was invisible: the filename carries a HASH, so two charts that
//--- should match and do not look merely "different" with no indication of WHICH field moved.
2026-08-01 11:27:28 -04:00
Print ( ID + " : config - " + IntegerToString ( m_hiddenLayersCount ) + " dense from " +
IntegerToString ( m_initialNeuronsCount ) + " units | batchnorm " +
( ( EnableBatchNorm & & BatchNormWindow > 1 ) ? " ON( " + IntegerToString ( BatchNormWindow ) + " ) " : " OFF " ) +
2026-08-22 00:25:52 -04:00
//--- "requested", not the bare number: the EFFECTIVE tau is capped against the head's
//--- usable logit range and cannot be known until the class priors are measured, so
//--- printing 1.00 here read as the value in force when every chart was actually running
//--- 0.35.
2026-08-01 11:27:28 -04:00
" | class-imbalance " + ( m_logitAdjustTau > 0.0
? " logit-adjust(tau " + DoubleToString ( m_logitAdjustTau , 2 ) + " requested) " : " OFF " ) +
" | input " + IntegerToString ( ( int ) m_historyBars * m_neuronsCount ) +
" ( " + IntegerToString ( ( int ) m_historyBars ) + " bars x " + IntegerToString ( m_neuronsCount ) + " ) " +
2026-08-22 00:25:52 -04:00
//--- The front-end stages are DERIVED (see ComputeConvFilterCount/ComputeLstmHiddenSize),
//--- so without them this "self-verifying" line verified only half the topology - it
//--- printed the dense taper while the conv/recurrent stages that actually dominate
//--- CONV/LSTM/HYBRID were invisible.
2026-08-01 11:27:28 -04:00
FrontEndConfigSummary ( ) ) ;
2026-08-22 00:25:52 -04:00
//--- Kept as its own line and deliberately free of any per-chart prefix INSIDE the string, so
//--- the six startup lines diff textually against each other.
2026-08-01 11:27:28 -04:00
Print ( ID + " : fingerprint - " + fp ) ;
//--- The resolved path is DebuggingMode-only: the tag above already names the folder (its m_id half)
//--- and the file's hash suffix (its hex half), so this line is derivable rather than new information,
//--- and a third startup line per chart is not worth spending on a user who will never open the file.
if ( DebuggingMode )
Print ( ID + " : model file - " + m_fileName + " .nnw " ) ;
//--- Strategy Tester / optimizer: target a LOCAL (agent-sandboxed, non-FILE_COMMON) cache file
//--- instead of the shared production weights, so genetic/complete optimization passes on this
//--- same agent can reuse an already-trained model whenever the topology-relevant inputs
//--- (neuron counts, layers, history bars, output count, opt algo, study period, ...) are
//--- unchanged from a previous pass, instead of re-running every training era from scratch each
//--- pass. The live/manual-chart production .nnw/.cfg under FILE_COMMON are never touched by
//--- this path, so a backtest can never corrupt or overwrite the deployed live model.
bool inTesterOrOpt = MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) ;
m_activeFileName = inTesterOrOpt ? ( m_fileName + " _optcache " ) : m_fileName ;
m_activeFileCommon = ! inTesterOrOpt ;
2026-08-22 00:25:52 -04:00
//--- Claim these files before anything reads or writes them, and refuse to start if another
2026-08-23 19:14:55 -04:00
//--- chart in this terminal already holds them (see AcquireConfigLock). SKIPPED under
//--- m_exportFeaturesOnly: that mode reads history and writes one CSV - it never trains, never
//--- saves a model (Warrior_EA.mq5's OnTick returns immediately, so no era ever completes) and
//--- therefore has nothing to protect against a concurrent chart.
if ( ! m_exportFeaturesOnly & & ! inTesterOrOpt & & ! AcquireConfigLock ( ) )
2026-08-01 11:27:28 -04:00
return false ;
2026-08-22 00:25:52 -04:00
//--- Any Strategy-Tester run - a single backtest OR an optimization pass - runs pure inference
//--- on the deployed model, never trains.
2026-08-01 11:27:28 -04:00
m_inferenceOnly = MQLInfoInteger ( MQL_TESTER ) ;
2026-08-22 00:25:52 -04:00
//--- Seed the agent-local optcache from the deployed production model on the first tester/opt
//--- pass. Re-seeds when the cache is MISSING *or* STALE. Copies FROM FILE_COMMON (the
//--- live/manual-chart model) INTO the agent-local sandbox only; the production files are read,
//--- never written, so a backtest still can't corrupt the deployed model.
2026-08-01 11:27:28 -04:00
bool cacheMissing = ! FileIsExist ( m_activeFileName + " .nnw " ) ;
bool cacheStale = false ;
if ( inTesterOrOpt & & ! cacheMissing & & FileIsExist ( m_fileName + " .nnw " , FILE_COMMON ) )
{
datetime prodModified = ( datetime ) FileGetInteger ( m_fileName + " .nnw " , FILE_MODIFY_DATE , true ) ;
datetime cacheModified = ( datetime ) FileGetInteger ( m_activeFileName + " .nnw " , FILE_MODIFY_DATE , false ) ;
//--- both timestamps must be readable before trusting the comparison; a 0 means "couldn't tell",
//--- and re-seeding on an unreadable timestamp every single pass would be worse than not checking.
cacheStale = ( prodModified > 0 & & cacheModified > 0 & & prodModified > cacheModified ) ;
if ( cacheStale )
Print ( __FUNCTION__ + " : the deployed model is newer than this agent's cached copy - re-seeding so the backtest runs the CURRENT model, not the previously cached one. " ) ;
}
if ( inTesterOrOpt & & ( cacheMissing | | cacheStale ) )
{
if ( FileIsExist ( m_fileName + " .nnw " , FILE_COMMON ) )
{
2026-08-22 00:25:52 -04:00
//--- The .nnw is the only copy that MUST succeed - retried (see CopyFileWithRetry's
//--- declaration comment) because a live chart's own atomic Save() can be mid-rename on
//--- this exact file.
2026-08-01 11:27:28 -04:00
if ( CopyFileWithRetry ( m_fileName + " .nnw " , m_activeFileName + " .nnw " ) )
{
2026-08-22 00:25:52 -04:00
//--- Best-effort sidecars: not retried - losing one just means a cold
//--- calibration/shadow-blend start rather than a wrong/untrained model, which the .nnw
//--- copy above already guards against.
2026-08-01 11:27:28 -04:00
if ( FileIsExist ( m_fileName + " .cfg " , FILE_COMMON ) )
CopySharedFile ( m_fileName + " .cfg " , m_activeFileName + " .cfg " , false ) ;
if ( FileIsExist ( m_fileName + " _shadow.nnw " , FILE_COMMON ) )
CopySharedFile ( m_fileName + " _shadow.nnw " , m_activeFileName + " _shadow.nnw " , false ) ;
//--- carry the calibration sidecar into the agent sandbox too, so a seeded backtest calibrates its
//--- live decisions with the deployed model's priors instead of the un-adjusted cold defaults.
if ( FileIsExist ( m_fileName + " .stats " , FILE_COMMON ) )
CopySharedFile ( m_fileName + " .stats " , m_activeFileName + " .stats " , false ) ;
Print ( __FUNCTION__ + " : seeded tester cache from the deployed production model ( " + m_fileName + " ) - this run reuses the deployed weights instead of retraining " ) ;
}
//--- else: CopyFileWithRetry already logged why. Fall through - the Net.Load() below will
//--- correctly report "no file" and BuildFreshTopology() takes over, same as a genuine first pass.
}
else if ( m_inferenceOnly )
2026-08-22 00:25:52 -04:00
//--- Name the exact file (symbol + timeframe + config fingerprint) it looked for: the
//--- model is keyed on the CHART TIMEFRAME, so the #1 cause of this is running the tester
//--- on a different timeframe than the model was trained on (e.g. an H4 model, tester set
//--- to H1) - which reads as "no model" when one exists under a different timeframe.
2026-08-01 11:27:28 -04:00
Print ( __FUNCTION__ + " : WARNING - no deployed production model found at ' " + m_fileName +
" .nnw' (shared folder) for " + _Symbol + " " + EnumToString ( ( ENUM_TIMEFRAMES ) _Period ) +
" . A single backtest runs inference only and will NOT train. Most common cause: the tester " +
" timeframe differs from the one the model was trained on (the filename is keyed on timeframe). " +
" Otherwise, train this configuration on a chart first, then re-run the backtest. " ) ;
}
if ( ! LoadAndCompareTopologyConfiguration ( m_activeFileName , m_initialNeuronsCount , m_hiddenLayersCount , m_neuronsReduction , m_minNeuronsCount , m_optimizationAlgo , m_historyBars , m_outputNeuronsCount , m_neuronsCount , m_minTrainYear , m_isInitialized , LEGACY_CONVERGE_WR_SLOT , m_fractalPeriods , m_convFilterCount , m_lstmHiddenSize , m_activeFileCommon ) )
{
2026-08-22 00:25:52 -04:00
//--- Topology/input params diverged from what produced the saved .nnw (or no .cfg exists yet;
//--- for inTesterOrOpt this is also the normal "first pass on this agent" case).
2026-08-01 11:27:28 -04:00
if ( FileIsExist ( m_activeFileName + " .nnw " , m_activeFileCommon ? FILE_COMMON : 0 ) )
{
Print ( __FUNCTION__ + " : " + m_activeFileName + " - topology/input params changed since last save; discarding incompatible saved weights and starting fresh " ) ;
FileDelete ( m_activeFileName + " .nnw " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
2026-08-22 00:25:52 -04:00
//--- Reaching here means a TRAINED model was just thrown away, so its drawn signals are
//--- stale for exactly the same reason ResetWeights() clears them: they would otherwise be
//--- restored moments later (LoadChartSignals runs at the end of this function) and shown
//--- as if they belonged to the model about to be trained.
2026-08-01 11:27:28 -04:00
ClearPersistedChartSignals ( " saved weights discarded - topology/input params changed " ) ;
}
if ( FileIsExist ( m_activeFileName + " _ckpt.tmp " , m_activeFileCommon ? FILE_COMMON : 0 ) )
FileDelete ( m_activeFileName + " _ckpt.tmp " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
// Same reasoning applies to the EMA shadow-weight file (see m_shadowNet's declaration comment) -
// it's shaped for the OLD topology too, and EnsureShadowNet() has no independent way to detect
// that mismatch on Load() (CNet::Load() doesn't cross-validate against an expected shape). Drop
// it so EnsureShadowNet() cleanly misses and re-bootstraps from the fresh Net instead.
if ( FileIsExist ( m_activeFileName + " _shadow.nnw " , m_activeFileCommon ? FILE_COMMON : 0 ) )
FileDelete ( m_activeFileName + " _shadow.nnw " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
//--- the calibration sidecar is tied to the discarded weights - drop it too so a fresh run
//--- re-measures priors from scratch instead of adjusting with a stale model's base rates.
if ( FileIsExist ( m_activeFileName + " .stats " , m_activeFileCommon ? FILE_COMMON : 0 ) )
FileDelete ( m_activeFileName + " .stats " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
2026-08-22 00:25:52 -04:00
//--- and the pattern-database backfill marker (see StartPatternDatabaseBackfill): it records
//--- the era of the model whose OOS calls were written into the ranking tables.
fix: the DB backfill could never run, and HEAD did not compile
Four defects in 64c5dd5/1a05e63, found by review + a baseline compile.
Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable.
1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were
declared `virtual bool ... override`, but CAppDialog declares both as
`virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151
on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was
never a success flag to forward. Verified: 0 errors, 0 warnings.
2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual
simulation (that one has been dead since it was written). Both are armed
at the instant convergence is declared, and both advance only from inside
Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick
ArmStudyEvent site sits in the `else` of a branch taken whenever
m_trainingComplete is set and m_trainRunActive is clear - which is exactly
the state FinalizeTrainRun() leaves behind one line before they are armed.
Train() was never called again, so the walks sat at their start index
forever: no "simulation complete" line, and not one row written to the DB
this feature exists to fill. Only a manual Resume/Retrain unstuck them.
Both flags now keep the model schedulable.
3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed.
Ensemble members deploy at Train() ENTRY and return immediately (so no era
is wasted), which skips the era-end block the backfill was started from.
All four members were a no-op for a second, independent reason. Armed on
the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff.
4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key,
no duplicate check - and m_dbBackfillDone is in-memory, so every later
attach that retrained to convergence wrote a second full set of rows for
the same bars. The ranking would count one bar once per model that ever
deployed, weighting superseded opinions as heavily as the live one. A
.dbfill marker stamps the deployed era; written only on completion (an
interrupted walk redoes itself rather than ranking a partial window) and
deleted with the other sidecars on reset-weights.
Also: WarmBlocking's timeout was silent, which restored the exact silent
pin failure it was added to prevent - it now says so in the journal, and
returns true for "no reference pairs to wait for" so the warning stays rare
enough to be read.
Not addressed, needs a decision: the backfill scores the OOS window with the
checkpoint that was SELECTED as best on that same window, then writes those
win rates into the table filter weights rank on - the selection set consumed
twice, undiscounted, while the deploy gate right next to it applies a
family-wise correction for exactly that effect. The rows are also simulated
triple-barrier outcomes at today's spread sharing a table with realised
fills. The completion log line now states both plainly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:25:51 -04:00
if ( FileIsExist ( m_activeFileName + " .dbfill " , m_activeFileCommon ? FILE_COMMON : 0 ) )
FileDelete ( m_activeFileName + " .dbfill " , m_activeFileCommon ? FILE_COMMON : 0 ) ;
2026-08-01 11:27:28 -04:00
SaveTopologyConfiguration ( m_activeFileName , m_initialNeuronsCount , m_hiddenLayersCount , m_neuronsReduction , m_minNeuronsCount , m_optimizationAlgo , m_historyBars , m_outputNeuronsCount , m_neuronsCount , LEGACY_STUDY_PERIOD_SLOT , m_minTrainYear , m_isInitialized , LEGACY_CONVERGE_WR_SLOT , m_fractalPeriods , m_convFilterCount , m_lstmHiddenSize , m_activeFileCommon ) ;
}
double loadedIndicatorParams [ ] ;
//--- Inference-only backtest: if this deployed model was validated MQL5-inference-safe at deploy
//--- (marker in its .stats), load it host-only and run the pure-MQL5 forward path so the backtest
//--- never loads WarriorDML/WarriorCPU.dll - no DLL file-lock class of failure, and the exact math
//--- the Market build ships. Falls back to a compute backend just below if that load fails.
if ( m_inferenceOnly & & CheckPointer ( Net ) ! = POINTER_INVALID )
{
LoadModelStats ( m_activeFileName , m_activeFileCommon ) ; // reads m_mqlInferenceValidated (and priors)
if ( m_mqlInferenceValidated )
{
Net . SetCpuInference ( true ) ;
PrintVerbose ( __FUNCTION__ + " : " + ID + " - inference-only backtest running pure-MQL5 (DLL-free): the deployed model is validated MQL5-inference-safe " ) ;
}
}
bool netLoaded = LoadNetWithRetry ( loadedIndicatorParams ) ;
//--- Pure-MQL5 load failed unexpectedly (should not happen for a validated model) - drop back to a
//--- compute backend and retry once so the backtest still runs via the DLL rather than on a fresh net.
if ( ! netLoaded & & CheckPointer ( Net ) ! = POINTER_INVALID & & Net . CpuInference ( ) )
{
Print ( __FUNCTION__ + " : " + ID + " - pure-MQL5 load failed; retrying with a compute backend (DLL) " ) ;
Net . SetCpuInference ( false ) ;
netLoaded = LoadNetWithRetry ( loadedIndicatorParams ) ;
}
//--- the file may carry a superseded architecture - correct it before anything reads the net
if ( netLoaded )
EnforceTopologyContract ( ) ;
2026-08-22 00:25:52 -04:00
//--- A superseded conv receptive field cannot be repaired in place (different weight-tensor shape),
//--- so the loaded net is discarded and the fresh-topology path below rebuilds and retrains.
2026-08-01 11:27:28 -04:00
if ( netLoaded & & m_topologySuperseded )
netLoaded = false ;
//--- restore the calibration sidecar (priors + confidence scale) that pairs with these weights, so a
//--- restart - including a buyer's inference-only backtest - calibrates live decisions exactly as the
//--- saved model did instead of running with cold defaults (priors 0 => no adjustment). See LoadModelStats().
if ( netLoaded )
LoadModelStats ( m_activeFileName , m_activeFileCommon ) ;
m_modelLoadedFromDisk = netLoaded ;
//--- Make a successful resume visible (the counterpart to the fresh-start / mismatch messages below):
//--- on a live chart this confirms the saved model was found and loaded rather than silently retrained.
if ( netLoaded & & ! inTesterOrOpt )
Print ( ID + " : resumed saved model from era " + IntegerToString ( m_eraCount ) + " (trainingComplete= " + ( string ) m_trainingComplete + " ) - continuing, not retraining from era 0. " ) ;
2026-08-13 10:23:11 -04:00
//--- RESUMED MODELS GET THE SAME WARM-UP AS FRESH ONES (2026-08-13; was `netLoaded ? 0 : 3`).
2026-08-22 00:25:52 -04:00
//--- Three no-op passes cost seconds. The label cache itself, however, is NEVER restored from
//--- the .nnw checkpoint - it lives only in the in-memory m_labelCacheBuy/Sell/HasValue arrays,
//--- which start empty every process start regardless of netLoaded.
2026-08-13 10:23:11 -04:00
m_warmupPassesRemaining = 3 ;
2026-08-01 11:27:28 -04:00
m_labelCachePrebuilt = false ;
if ( inTesterOrOpt & & netLoaded )
Print ( __FUNCTION__ + " : " + ID + " - reused cached weights from a previous optimization/tester pass on this agent (era " + IntegerToString ( m_eraCount ) + " , trainingComplete= " + ( string ) m_trainingComplete + " ) - skipping redundant training for this unchanged config " ) ;
if ( netLoaded & & ArraySize ( loadedIndicatorParams ) = = AD_TUNE_PARAM_COUNT )
{
2026-08-22 00:25:52 -04:00
//--- Restart deploying previously AutoTune-d indicator params even with
//--- AutoTuneIndicators=false now.
2026-08-13 10:23:11 -04:00
AdoptIndicatorParams ( loadedIndicatorParams , indicators ) ;
2026-08-01 11:27:28 -04:00
}
if ( ! netLoaded )
{
int error_code = GetLastError ( ) ;
//--- Do NOT present error_code as the cause: on a no-GPU/CPU-DLL box it is the harmless 5100
2026-08-22 00:25:52 -04:00
//--- (OpenCL-not-found) left by the compute probe inside CNet::Load, NOT the reason the file
//--- was rejected.
2026-08-01 11:27:28 -04:00
if ( error_code ! = 5004 ) // not "file not found"
ResetLastError ( ) ;
2026-08-22 00:25:52 -04:00
//--- CRITICAL: a failed load may have ALREADY overwritten the training-state out-params from
//--- the bad file's header before it was rejected - notably a corrupt/empty 0-layer stub
//--- whose header still says trainingComplete=1 (see CNet::Load's 0-layer guard).
2026-08-01 11:27:28 -04:00
m_trainingComplete = false ;
m_eraCount = 0 ;
dtStudied = 0 ;
dForecast = 0 ;
2026-08-22 00:25:52 -04:00
//--- Cold the in-memory calibration so the freshly-rebuilt (untrained) topology below runs
//--- with no stale prior-correction until a retrain re-measures it (priors 0 =>
//--- AdjustedSignalFromSoftmax is a no-op; scale 1.0 = the constructor default).
2026-08-01 11:27:28 -04:00
m_priorBuy = 0.0 ;
m_priorSell = 0.0 ;
m_priorNeutral = 0.0 ;
m_confidenceCalScale = 1.0 ;
//--- Accurate diagnostic (do NOT cite GetLastError() - inside CNet::Load the OpenCL probe leaves 5100
//--- there on a no-GPU/CPU-DLL box, which has nothing to do with the file). Distinguish an ordinary
//--- fresh start (no file yet) from a real read failure of an existing file by testing existence.
if ( ! inTesterOrOpt )
{
int loadFlags = m_activeFileCommon ? FILE_COMMON : 0 ;
if ( FileIsExist ( m_activeFileName + " .nnw " , loadFlags ) )
Print ( ID + " : could not read the existing model file " + m_activeFileName + " .nnw - rebuilding a fresh topology to retrain from era 0. Existing .stats/_shadow.nnw are KEPT (they refresh as training runs). If this recurs, that .nnw is likely corrupt - back it up, then use the panel's reset-weights to start clean. " ) ;
else
Print ( ID + " : no saved model for this config yet - starting a fresh training run from era 0. " ) ;
}
2026-08-22 00:25:52 -04:00
//--- Re-seed before building a fresh topology so weight init is genuinely random. See
//--- System\Random.mqh. Matches ResetWeights() and OnInit.
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:
WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.
SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.
CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.
AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.
Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.
The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
WarriorRandSeed ( ID ) ;
2026-08-01 11:27:28 -04:00
//--- Era 0 with no weights behind it, so any arrow currently on this chart was drawn by a
2026-08-22 00:25:52 -04:00
//--- DIFFERENT model - the previous fingerprint's, or a corrupt .nnw's. Deliberately at
//--- this call site rather than inside BuildFreshTopology(): the genetic tuner calls that
//--- for every throwaway candidate (AutoTune.mqh) and must not touch the chart.
2026-08-01 11:27:28 -04:00
ClearPersistedChartSignals ( " fresh topology at era 0 - arrows belong to a previous model " ) ;
if ( ! BuildFreshTopology ( ) )
return false ;
}
TempData = new CArrayDouble ( ) ;
if ( CheckPointer ( TempData ) = = POINTER_INVALID )
return false ;
if ( netLoaded )
// Populate dPrevSignal from the just-loaded weights immediately, rather than leaving it at
// its blank constructor default until the next (asynchronous, queued) training pass happens
// to run - matters most for the tester cache-reuse path above, where training may be skipped
// entirely for this run because dtStudied already covers the whole backtest window.
RefreshLatestSignal ( ) ;
//--- Status line must match what the gate below (if(!m_trainingComplete && !m_inferenceOnly)) will
//--- actually do - otherwise an inference-only single backtest logs "resuming full training now" right
//--- under the "runs inference only and will NOT train" warning, which reads as a contradiction.
string trainState = m_trainingComplete
? " already complete - staying converged, no full retrain on this restart "
: ( m_inferenceOnly
? " NOT complete, but this is an inference-only backtest - NOT training (see warning above); deploy a trained model for meaningful results "
: " NOT complete (interrupted or never converged) - resuming full training now " ) ;
Print ( __FUNCTION__ + " : " + m_activeFileName + " - training " + trainState ) ;
//--- Only kick off a full Train() run here if the loaded model genuinely isn't converged yet - an
//--- already-complete model used to get one full era-loop retrain (real Net.backProp() over the
//--- whole IS window) on every single EA restart/reattach for no reason, since this "Init" event
//--- bypassed ScheduleTrainingIfNeeded()'s m_trainingComplete gate entirely. dPrevSignal is already
//--- fresh from RefreshLatestSignal() above; ScheduleTrainingIfNeeded()'s normal per-tick check
//--- will call RefreshConvergedSignal() itself once a genuinely new bar closes.
if ( ! m_trainingComplete & & ! m_inferenceOnly )
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
ArmStudyEvent ( ( long ) MathMax ( 0 , MathMin ( iTime ( _Symbol , PERIOD_CURRENT , ( int ) ( 100 * Net . recentAverageSmoothingFactor * ( m_trainingComplete ? 1 : 10 ) ) ) , dtStudied ) ) , " Init " ) ;
2026-08-01 11:27:28 -04:00
//--- Restore arrows persisted from a previous session (see SaveChartSignals). MUST run here, not in
//--- InitIndicators(): the arrows file is keyed on the FULL m_fileName including the per-config
//--- fingerprint, which is only appended above - see the note left at InitIndicators()'s old call site.
LoadChartSignals ( ) ;
//--- bootstrap (or restore) the EMA shadow net now rather than waiting for the first
//--- RefreshLatestSignal()/era-blend call to lazily trigger it - see m_shadowNet's declaration
//--- comment.
EnsureShadowNet ( ) ;
m_isInitialized = true ;
2026-08-23 19:14:55 -04:00
//--- RESEARCH ONLY, under m_exportFeaturesOnly. Runs here because this is the first point at which
//--- the indicators, the buffers and the derived barrier horizon are all settled, and it needs no
//--- model, no labels and no training.
if ( m_exportFeaturesOnly )
ExportFeatureMatrix ( ) ;
2026-08-01 11:27:28 -04:00
return true ;
}
//+------------------------------------------------------------------+
//| Shared training-set size estimate - see the declaration comment. |
//+------------------------------------------------------------------+
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
double CExpertSignalAIBase : : EstimatedInSampleBarsRaw ( void ) const
2026-08-01 11:27:28 -04:00
{
int secs = PeriodSeconds ( m_period ) ;
if ( secs < = 0 )
secs = PeriodSeconds ( PERIOD_H1 ) ;
double barsPerYear = ( SECONDS_PER_YEAR / ( double ) secs ) * MARKET_OPEN_FRACTION ;
double oosKept = ( 100.0 - ( double ) m_oosSplitPct ) / 100.0 ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
datetime firstAvailableBar = ( datetime ) SeriesInfoInteger ( _Symbol , m_period , SERIES_FIRSTDATE ) ;
MqlDateTime floorTime ;
TimeCurrent ( floorTime ) ;
floorTime . year = m_minTrainYear ;
floorTime . mon = 1 ;
floorTime . day = 1 ;
floorTime . hour = 0 ;
floorTime . min = 0 ;
floorTime . sec = 0 ;
datetime windowStart = StructToTime ( floorTime ) ;
if ( firstAvailableBar > windowStart )
windowStart = firstAvailableBar ;
int available = Bars ( _Symbol , m_period , windowStart , TimeCurrent ( ) ) ;
2026-08-22 00:25:52 -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.
2026-08-01 11:27:28 -04:00
if ( available < TOPOLOGY_BUDGET_MIN_TRUSTED_BARS )
{
Print ( ID + " : WARNING - only " + IntegerToString ( available ) + " bars of " + _Symbol +
" history are available yet, too few to size the network from. Falling back to a " +
IntegerToString ( TOPOLOGY_BUDGET_FALLBACK_YEARS ) + " -year assumption. If this is a fresh " +
" install, let the terminal finish downloading history and then delete this model's weights " +
" from the panel so the topology is sized from the real data. " ) ;
return ( double ) TOPOLOGY_BUDGET_FALLBACK_YEARS * barsPerYear * oosKept ;
}
return ( double ) available * oosKept ;
}
//+------------------------------------------------------------------+
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
//| In-sample budget in INDEPENDENT observations - see declaration. |
//+------------------------------------------------------------------+
double CExpertSignalAIBase : : EstimatedInSampleBars ( void ) const
{
2026-08-22 00:25:52 -04:00
//--- DEFLATED BY LABEL OVERLAP (2026-08-19). 4 - the same correction every standard error in
//--- this file already applies).
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
return EffectiveSampleSize ( EstimatedInSampleBarsRaw ( ) ) ;
}
//+------------------------------------------------------------------+
//| Fan-in of the first dense layer - see the declaration comment. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : FirstLayerFanIn ( void ) const
{
int frontEndOut = UsesLstmStage ( ) ? m_lstmHiddenSize
: ( UsesConvStage ( ) ? ConvOutputWidth ( ) : 0 ) ;
return ( frontEndOut > 0 ) ? frontEndOut : ( int ) m_historyBars * m_neuronsCount ;
}
//+------------------------------------------------------------------+
2026-08-11 21:53:37 -04:00
//| Derived input-window length - see the declaration comment. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : DeriveHistoryBars ( void )
{
int availableBars = Bars ( _Symbol , m_period ) ;
int span = ( int ) MathMin ( availableBars - 1 , WINDOW_DERIVE_SPAN_BARS ) ;
if ( span < TOPOLOGY_BUDGET_MIN_TRUSTED_BARS )
{
Print ( ID + " : WARNING - only " + IntegerToString ( availableBars ) + " bars of " + _Symbol +
" history are available yet, too few to measure the input window from. Falling back to " +
IntegerToString ( HISTORY_BARS_FALLBACK ) + " bars. If this is a fresh install, let history "
" finish downloading and delete this model's weights so the window is measured from real data. " ) ;
return HISTORY_BARS_FALLBACK ;
}
//--- newest CLOSED bars only (start 1): the forming bar's extremes are still moving
double hi [ ] , lo [ ] ;
ArraySetAsSeries ( hi , true ) ;
ArraySetAsSeries ( lo , true ) ;
if ( CopyHigh ( _Symbol , m_period , 1 , span , hi ) ! = span | |
CopyLow ( _Symbol , m_period , 1 , span , lo ) ! = span )
{
Print ( ID + " : WARNING - could not read " + IntegerToString ( span ) + " bars to measure the input "
" window; falling back to " + IntegerToString ( HISTORY_BARS_FALLBACK ) + " bars. " ) ;
return HISTORY_BARS_FALLBACK ;
}
//--- Swing pivots: strict local extremum against the NEWER side, >= against the older side (the
2026-08-22 00:25:52 -04:00
//--- standard tie-break so a flat top counts once).
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
double legs [ ] ;
2026-08-11 21:53:37 -04:00
ArrayResize ( legs , 0 , 256 ) ;
int lastType = 0 ; // +1 swing high, -1 swing low, 0 none yet
int lastPivotBar = -1 ;
double lastExtreme = 0.0 ;
for ( int b = span - 1 - WINDOW_SWING_WING ; b > = WINDOW_SWING_WING ; b - - ) // oldest -> newest
{
bool isHigh = true , isLow = true ;
for ( int w = 1 ; w < = WINDOW_SWING_WING & & ( isHigh | | isLow ) ; w + + )
{
if ( hi [ b ] < = hi [ b - w ] | | hi [ b ] < hi [ b + w ] )
isHigh = false ;
if ( lo [ b ] > = lo [ b - w ] | | lo [ b ] > lo [ b + w ] )
isLow = false ;
}
int type = 0 ;
if ( isHigh ! = isLow )
type = isHigh ? 1 : -1 ; // a bar that is both is degenerate; skip it
if ( type = = 0 )
continue ;
if ( type = = lastType )
{
double x = ( type = = 1 ) ? hi [ b ] : lo [ b ] ;
if ( ( type = = 1 & & x > lastExtreme ) | | ( type = = -1 & & x < lastExtreme ) )
{
lastPivotBar = b ;
lastExtreme = x ;
}
continue ;
}
if ( lastType ! = 0 )
{
int n = ArraySize ( legs ) ;
ArrayResize ( legs , n + 1 , 256 ) ;
legs [ n ] = lastPivotBar - b ; // series indices: newer bar = smaller index
}
lastType = type ;
lastPivotBar = b ;
lastExtreme = ( type = = 1 ) ? hi [ b ] : lo [ b ] ;
}
if ( ArraySize ( legs ) < WINDOW_DERIVE_MIN_LEGS )
{
Print ( ID + " : WARNING - only " + IntegerToString ( ArraySize ( legs ) ) + " confirmed swing legs in " +
IntegerToString ( span ) + " bars, too few to trust a median. Falling back to " +
IntegerToString ( HISTORY_BARS_FALLBACK ) + " bars. " ) ;
return HISTORY_BARS_FALLBACK ;
}
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
double median = MathMedian ( legs ) ;
2026-08-11 21:53:37 -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 + + )
if ( ladder [ i ] < = median )
window = ladder [ i ] ;
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
PrintFormat ( " %s: derived input window - %d bars (median confirmed swing leg %.1f over %d legs in %d "
2026-08-11 21:53:37 -04:00
" bars, snapped down to the ladder%s). Measured once at model creation and pinned in the "
" .cfg; an existing model adopts its own trained window instead. " ,
ID , window , median , ArraySize ( legs ) , span ,
median > 32 ? " , CAPPED at 32 - era time scales with the window " : " " ) ;
return window ;
}
//+------------------------------------------------------------------+
2026-08-01 11:27:28 -04:00
//| Dense-taper depth - see the declaration comment. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ComputeHiddenLayerCount ( void ) const
{
//--- Diagnostic escape hatch (compile-time, see ForceHiddenLayers). Deliberately not an input: this
//--- exists to run depth comparisons while working on the EA, and a user who picks a depth is
//--- contradicting the width and taper the code derived around it.
if ( ForceHiddenLayers > 0 )
return ( int ) MathMax ( 1 , MathMin ( MAX_HIDDEN_LAYERS , ForceHiddenLayers ) ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
int lastHidden = ( int ) MathMax ( HIDDEN_TAPER_OUTPUT_MULTIPLE * m_outputNeuronsCount , HIDDEN_TAPER_MIN_WIDTH ) ;
lastHidden = ( int ) MathMin ( lastHidden , m_initialNeuronsCount ) ;
if ( lastHidden < = 0 | | m_initialNeuronsCount < = lastHidden )
feat: derived taper restored; DB ranking reads a reserved slice, shrunk
TOPOLOGY - reverts the two constants and drops CausalHiddenLayerFloor.
The MQL5 article's 30%-per-layer cut and floor of 20 are load-bearing on ITS
first-layer width of 1000 (1000->300->90->27 needs a floor to stop). This
codebase MEASURES that width, and on the live SP500 H4 config it is 16 units -
already floored, with the budget printing "11360 estimated in-sample bars
cannot support a 800-wide input ... roughly 1.1 weights per training bar -
expect overfitting". At 16 units a floor of 20 makes lastHidden >=
m_initialNeuronsCount, so ComputeHiddenLayerCount returns on its first branch
and the width taper - the only part derived from this symbol's data - became
dead code on all four ensemble members, with depth (2 -> 4) set entirely by
counting feature domains. ComputeLayerWidths had already rejected this exact
pair of constants in its own comment.
The causal floor's premise does not hold either: layers are not inference
steps. The "1 layer linear / 2 nonlinear / 3 multi-connected" result is
Lippmann 1987 and is about hard-threshold units; with sigmoid/ReLU, Cybenko
1989 and Hornik 1991 give universal approximation from a single hidden layer.
Depth buys parameter efficiency for compositional functions, not reasoning
hops. ForceHiddenLayers remains for measuring depth directly.
RANKING SLICE - the backfill no longer reads the window it is judged on.
The deployed checkpoint is CHOSEN as the best-scoring era on the OOS window,
so win rates measured back over it are selection-inflated, and the backfill
was writing exactly those into the table filter weights rank on: the
selection set consumed twice, beside a deploy gate that applies a Sidak
correction for that effect. The newest RANK_SLICE_PCT_OF_OOS (20%) of the OOS
window, plus a label-horizon purge, is now reserved and graded by nothing -
not pass 3, not checkpoint selection, not the gate. The backfill reads only
that. The gate keeps ~80% of its measurement (power goes as the square root,
so ~10% of a sigma), and the slice is the newest data, which is the regime
about to be traded. RankSliceBars returns 0 when no honest slice fits and the
backfill then REFUSES and says so, rather than falling back to the scoring
window and looking like a success.
SHRINKAGE - per-tier win rates are shrunk toward the filter's own pooled rate
by MIN_TRADES_FOR_WIN_RATE pseudo-trades before becoming weights. The raw
ratio at the minimum sample count carries a ~15pp standard error, so a tier
that went 8-2 was handed weight 80 and outranked a tier measured over
hundreds of calls at 55 - the ranking was being driven by which small tier got
lucky. Opt-in per call site (priorWeight 0 keeps the raw behaviour).
Compile-verified: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:49:52 -04:00
return MIN_HIDDEN_LAYERS ;
2026-08-01 11:27:28 -04:00
double steps = MathLog ( ( double ) m_initialNeuronsCount / ( double ) lastHidden ) / MathLog ( HIDDEN_TAPER_TARGET_RATIO ) ;
int layers = ( int ) MathRound ( steps ) + 1 ; // +1: the first layer IS the starting endpoint, not a step
return ( int ) MathMax ( MIN_HIDDEN_LAYERS , MathMin ( MAX_HIDDEN_LAYERS , layers ) ) ;
}
//+------------------------------------------------------------------+
//| Conv output-filter count - see the declaration comment. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ComputeConvFilterCount ( void ) const
{
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
int chosen = ( ConvReceptiveFieldBars ( ) * m_neuronsCount ) / CONV_COMPRESSION_DIVISOR ;
//--- Snap DOWN to a power-of-two ladder for the same reason the first-layer width does: the target is
//--- approximate, and a value that moves with every feature toggle would re-key the weights file more
//--- often than the change in capacity justifies.
int ladder [ ] = { 4 , 8 , 16 , 32 } ;
int snapped = CONV_FILTERS_MIN ;
for ( int i = 0 ; i < ArraySize ( ladder ) ; i + + )
if ( ladder [ i ] < = chosen )
snapped = ladder [ i ] ;
return ( int ) MathMax ( CONV_FILTERS_MIN , MathMin ( CONV_FILTERS_MAX , snapped ) ) ;
}
//+------------------------------------------------------------------+
//| Derived front-end stages, for the startup config line. |
//+------------------------------------------------------------------+
string CExpertSignalAIBase : : FrontEndConfigSummary ( void ) const
{
string s = " " ;
//--- conv slides a ConvReceptiveFieldBars()-bar window one bar at a time, emitting m_convFilterCount
//--- filters per position; the optional channel pool + second conv follow. Reported from the shape
//--- helpers rather than re-derived, so this line always describes what AddConvStage actually built.
if ( UsesConvStage ( ) )
{
s + = " | conv " + IntegerToString ( ConvReceptiveFieldBars ( ) ) + " bars x " +
IntegerToString ( m_neuronsCount ) + " -> " + IntegerToString ( m_convFilterCount ) +
" ( " + IntegerToString ( ConvFirstStagePositions ( ) ) + " pos) " ;
if ( HasSecondConvStage ( ) )
s + = " | pool / " + IntegerToString ( m_convFilterCount ) +
" | conv2 -> " + IntegerToString ( ConvOutputPositions ( ) ) + " pos x " +
IntegerToString ( m_convFilterCount ) + " = " + IntegerToString ( ConvOutputWidth ( ) ) ;
else
s + = " = " + IntegerToString ( ConvOutputWidth ( ) ) ;
}
if ( UsesLstmStage ( ) )
s + = " | lstm " + IntegerToString ( LstmFanIn ( ) ) + " -> " + IntegerToString ( m_lstmHiddenSize ) ;
//--- The dense stack is budgeted against the RAW input, so on any topology with a front-end it can be
//--- WIDER than the vector reaching it - a linear fan-out that cannot recover information the
//--- bottleneck already discarded, only add parameters. Flag it rather than silently reshaping a
//--- trained topology; see ComputeFirstLayerWidth.
int frontEndOut = UsesLstmStage ( ) ? m_lstmHiddenSize
: ( UsesConvStage ( ) ? ConvOutputWidth ( ) : 0 ) ;
if ( frontEndOut > 0 & & m_initialNeuronsCount > frontEndOut )
s + = " | NOTE dense fans out " + IntegerToString ( frontEndOut ) + " -> " +
IntegerToString ( m_initialNeuronsCount ) ;
return s ;
}
//+------------------------------------------------------------------+
//| Input width the LSTM block actually receives. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : LstmFanIn ( void ) const
{
//--- LSTM-only: the layer sits directly on the input, so it sees the whole flattened vector.
2026-08-22 00:25:52 -04:00
//--- HYBRID: AddConvStage runs first, so the LSTM sees the CONV FEATURE MAP, not the input.
2026-08-01 11:27:28 -04:00
if ( HasConvBeforeLstm ( ) )
return ConvOutputWidth ( ) ;
return ( int ) m_historyBars * m_neuronsCount ;
}
//+------------------------------------------------------------------+
//| LSTM recurrent hidden width - see the declaration comment. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ComputeLstmHiddenSize ( void ) const
{
//--- The LSTM block's parameter count is EXACTLY 4 * H * (H + inputs + 1) - see
//--- CNeuronLSTMOCL::SetInputs in AI\Network.mqh - and AddLstmStage feeds it the whole flattened
2026-08-22 00:25:52 -04:00
//--- input vector, so `inputs` is historyBars x neuronsCount.
2026-08-01 11:27:28 -04:00
int inputs = ( LSTM_SEQUENCE_MODE ? ( HasConvBeforeLstm ( ) ? m_convFilterCount : m_neuronsCount )
: LstmFanIn ( ) ) ;
double isBars = EstimatedInSampleBars ( ) ;
if ( inputs < = 0 | | isBars < = 0.0 )
return LSTM_HIDDEN_MIN ;
double b = ( double ) ( inputs + 1 ) ;
double budget = ( - b + MathSqrt ( b * b + 4.0 * ( isBars / 4.0 ) ) ) / 2.0 ;
int ladder [ ] = { 8 , 16 , 32 , 64 , 128 } ;
int snapped = LSTM_HIDDEN_MIN ;
for ( int i = 0 ; i < ArraySize ( ladder ) ; i + + )
if ( ( double ) ladder [ i ] < = budget )
snapped = ladder [ i ] ;
return ( int ) MathMax ( LSTM_HIDDEN_MIN , MathMin ( LSTM_HIDDEN_MAX , snapped ) ) ;
}
//+------------------------------------------------------------------+
//| Capacity budget for the first dense layer - see the declaration. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ComputeFirstLayerWidth ( void ) const
{
2026-08-22 00:25:52 -04:00
//--- 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.
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
int frontEndOut = UsesLstmStage ( ) ? m_lstmHiddenSize
: ( UsesConvStage ( ) ? ConvOutputWidth ( ) : 0 ) ;
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
//--- 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 ( ) ;
2026-08-01 11:27:28 -04:00
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 ] ;
2026-08-22 00:25:52 -04:00
//--- 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.
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
if ( frontEndOut > 0 )
chosen = ( int ) MathMin ( chosen , frontEndOut ) ;
2026-08-22 00:25:52 -04:00
//--- 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.
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
double lifespan = MeanLabelLifespan ( ) ;
string basis = ( lifespan > 1.0001 )
? StringFormat ( " %.0f independent in-sample observations (%.0f bars / mean label "
" lifespan %.1f) " , isBars , EstimatedInSampleBarsRaw ( ) , lifespan )
: StringFormat ( " %.0f estimated in-sample bars (label overlap NOT YET MEASURED, so "
" this is an UPPER BOUND - see the CAPACITY line once labels exist) " ,
isBars ) ;
2026-08-01 11:27:28 -04:00
if ( budget < FIRST_LAYER_MIN_WIDTH )
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
Print ( ID + " : WARNING - " + basis + " cannot support a " +
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
IntegerToString ( inputWidth ) + " -wide " +
( frontEndOut > 0 ? " vector into the dense stack " : " input " ) + " . The first layer is being floored at " +
2026-08-01 11:27:28 -04:00
IntegerToString ( FIRST_LAYER_MIN_WIDTH ) + " units, which is still roughly " +
DoubleToString ( ( double ) ( inputWidth + 1 ) * FIRST_LAYER_MIN_WIDTH / MathMax ( 1.0 , isBars ) , 1 ) +
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.
EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.
SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.
The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.
Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.
Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).
NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
" weights per independent observation - expect overfitting. Reduce HistoryBars or the " +
" feature set, lengthen the study period, pool instruments, or train on a lower timeframe. " ) ;
2026-08-01 11:27:28 -04:00
return MathMax ( FIRST_LAYER_MIN_WIDTH , chosen ) ;
}
//+------------------------------------------------------------------+
//| Batch-normalization layer - see the declaration comment. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : AddBatchNormStage ( CArrayObj * topology , int units )
{
if ( CheckPointer ( topology ) = = POINTER_INVALID )
return false ;
//--- Not an error: the input is off, so the topology simply has no normalization layers. Returning
//--- true keeps every call site a plain `if(!Add...) return false;` with no extra branching.
if ( ! EnableBatchNorm )
return true ;
//--- A window of 1 makes the layer a no-op passthrough (mean==x, variance==0), which is a silently
//--- useless layer rather than an obviously absent one. Refuse to build it instead.
if ( BatchNormWindow < = 1 )
return true ;
CLayerDescription * desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
return false ;
desc . count = units ;
desc . type = defNeuronBatchNorm ;
desc . batch = BatchNormWindow ;
//--- Identity forward transform. The non-linearity belongs to the dense layer stacked on top of this
//--- one; normalizing and then squashing in the same step would undo the normalization.
desc . activation = NONE ;
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
if ( ! topology . Add ( desc ) )
{
delete desc ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Convolution front-end: conv -> channel pool -> conv. Shared by |
//| CSignalCONV and CSignalHYBRID - see the declaration comment. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : AddConvStage ( CArrayObj * topology )
{
if ( CheckPointer ( topology ) = = POINTER_INVALID )
return false ;
//--- Stage 1: convolution across CONV_RECEPTIVE_FIELD_BARS bars, advancing one bar at a time.
CLayerDescription * desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
return false ;
//--- desc.count here is the conv layer's own output-filter count (CNeuronConvOCL::Init's window_out
//--- param, AI\Network.mqh) - was m_hiddenLayersCount (an unrelated dense-taper-depth setting,
//--- defaulting to 4), bottlenecking every sliding position to just 4 filters regardless of how wide
//--- the rest of the network was. See ConvFilterCount's declaration comment (Variables\Inputs.mqh).
desc . count = m_convFilterCount ;
desc . type = defNeuronConv ;
// PRELU, not TANH: matches what CNeuronConv's CPU path (Network.mqh) has always hardcoded
// regardless of this setting (its activationFunction() override ignores `activation` entirely) -
refactor(ai): remove the DirectML/D3D12 GPU compute tier (S1.5)
Three backends left, as the operator specified: OpenCL, the CPU DLL,
and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via
WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the
tier being removed here; the CPU DLL tier - the one actually used on
the training machine (no OpenCL, no DirectML) - is untouched.
AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import
block and COMPUTE_TIER_GPU (checked first that nothing persists the
enum value and only one external site reads .Tier() - safe), collapsed
every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call.
Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(),
member directml/DirectML->computeDll/ComputeDll across every AI/ file
that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh.
NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code
switch and the now-impossible GPU-tier log branch.
Verified via per-file brace-balance diff against HEAD and a whole-repo
grep for every removed symbol (CDirectMLMy/InitDirectML/
COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional
historical-note comment in the new file's header.
DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++
source, left in place pending an operator decision. Architecture docs
(AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the
4-backend/GPU-tier shape and are not updated in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:32:09 -04:00
// 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.
2026-08-01 11:27:28 -04:00
desc . activation = PRELU ;
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
//--- The whole point: a window spanning several bars. Guarded because m_historyBars can be small
//--- enough that a multi-bar window would not fit at all, in which case this degrades to the old
//--- per-bar projection rather than building a negative-width layer.
desc . window = ConvReceptiveFieldBars ( ) * m_neuronsCount ;
desc . step = m_neuronsCount ;
if ( ! topology . Add ( desc ) )
{
delete desc ;
return false ;
}
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
return true ;
}
//+------------------------------------------------------------------+
//| Conv chain shape. SINGLE SOURCE OF TRUTH - AddConvStage builds |
//| from these and LstmFanIn/FrontEndConfigSummary report from them, |
//| so what is constructed and what is logged cannot drift apart. |
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ConvReceptiveFieldBars ( void ) const
{
//--- Degrade to a per-bar projection rather than build an impossible layer when history is too short
//--- for a multi-bar window. MathMin against m_historyBars keeps window <= input width.
int bars = ( int ) MathMin ( ( int ) CONV_RECEPTIVE_FIELD_BARS , ( int ) m_historyBars ) ;
return ( bars > 0 ? bars : 1 ) ;
}
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ConvFirstStagePositions ( void ) const
{
//--- Sliding positions of stage 1: window ConvReceptiveFieldBars() bars, step 1 bar.
int p = ( int ) m_historyBars - ( ConvReceptiveFieldBars ( ) - 1 ) ;
return ( p > 0 ? p : 1 ) ;
}
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : HasSecondConvStage ( void ) const
{
//--- Permanently false: the conv chain is ONE true convolution. Kept (rather than deleted along with the
//--- pool + second conv it used to gate) so ConvOutputPositions/ConvOutputWidth stay the single source of
//--- truth for the chain's shape and a future strided second stage has one place to switch itself on.
return false ;
}
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ConvOutputPositions ( void ) const
{
int p = ConvFirstStagePositions ( ) ;
return ( HasSecondConvStage ( ) ? p - ( ConvReceptiveFieldBars ( ) - 1 ) : p ) ;
}
//+------------------------------------------------------------------+
int CExpertSignalAIBase : : ConvOutputWidth ( void ) const
{
//--- Total element count reaching whatever is stacked above the conv chain: the conv output is
//--- position-major, window_out filters per position.
return ConvOutputPositions ( ) * m_convFilterCount ;
}
//+------------------------------------------------------------------+
//| LSTM sequence stage. Shared by CSignalLSTM and CSignalHYBRID - |
//| see the declaration comment. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : AddLstmStage ( CArrayObj * topology )
{
if ( CheckPointer ( topology ) = = POINTER_INVALID )
return false ;
CLayerDescription * desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
return false ;
desc . count = m_lstmHiddenSize ;
desc . type = defNeuronLSTM ;
desc . activation = TANH ;
//--- CNeuronLSTMOCL now has an accelerated SGD+momentum kernel (LSTM_UpdateWeightsMomentum,
refactor(ai): remove the DirectML/D3D12 GPU compute tier (S1.5)
Three backends left, as the operator specified: OpenCL, the CPU DLL,
and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via
WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the
tier being removed here; the CPU DLL tier - the one actually used on
the training machine (no OpenCL, no DirectML) - is untouched.
AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import
block and COMPUTE_TIER_GPU (checked first that nothing persists the
enum value and only one external site reads .Tier() - safe), collapsed
every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call.
Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(),
member directml/DirectML->computeDll/ComputeDll across every AI/ file
that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh.
NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code
switch and the now-impossible GPU-tier log branch.
Verified via per-file brace-balance diff against HEAD and a whole-repo
grep for every removed symbol (CDirectMLMy/InitDirectML/
COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional
historical-note comment in the new file's header.
DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++
source, left in place pending an operator decision. Architecture docs
(AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the
4-backend/GPU-tier shape and are not updated in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:32:09 -04:00
//--- AI\Network.mqh/Network.cl/DirectML\WarriorCPU.cpp) alongside the original
2026-08-01 11:27:28 -04:00
//--- Adam one, so this layer honors the same TrainingOptimizer input as PAI/CONV - see
//--- m_optimizationAlgo's declaration comment.
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
desc . window = ( LSTM_SEQUENCE_MODE ? ( HasConvBeforeLstm ( ) ? m_convFilterCount : m_neuronsCount ) : 0 ) ;
//--- MathMax(1,...) guard taken from the HYBRID copy: the CSignalLSTM copy divided unguarded, so a
//--- historyBars of 1 produced step 0 there and step 1 here for what is meant to be the same layer.
desc . step = MathMax ( 1 , ( int ) m_historyBars / 2 ) ;
if ( ! topology . Add ( desc ) )
{
delete desc ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| Builds a fresh, untrained topology into Net - the exact layer |
//| construction InitNeuralNetwork() used to inline for the |
//| "no saved .nnw" case; factored out so TuneIndicatorsAndTrain() can|
//| get a clean-slate Net per trial without touching indicator init. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : BuildFreshTopology ( )
{
CArrayObj * Topology = new CArrayObj ( ) ;
if ( CheckPointer ( Topology ) = = POINTER_INVALID )
return false ;
//--- Input Layer
CLayerDescription * desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
{
delete Topology ;
return false ;
}
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- NetInputWidth = the bar window plus (meta target only) the per-candidate setup descriptor
//--- appended after it - see AppendCandidateFeatures. Zero-delta for every direction model.
desc . count = NetInputWidth ( ) ;
2026-08-01 11:27:28 -04:00
desc . type = defNeuron ;
desc . activation = NONE ;
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
if ( ! Topology . Add ( desc ) )
{
delete Topology ;
return false ;
}
//--- neuron-type-specific layers (Conv+Pool, LSTM, or none for a plain perceptron)
if ( ! AddCustomLayers ( Topology ) )
{
delete Topology ;
return false ;
}
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
int lastHidden = MathMax ( HIDDEN_TAPER_OUTPUT_MULTIPLE * m_outputNeuronsCount , HIDDEN_TAPER_MIN_WIDTH ) ;
//--- Never wider than where the taper starts: a narrow first layer (see the D1 case in
//--- ComputeFirstLayerWidth) must still funnel DOWN, not fan back out.
lastHidden = MathMin ( lastHidden , m_initialNeuronsCount ) ;
double taperRatio = ( m_hiddenLayersCount > 1 )
? MathPow ( ( double ) lastHidden / ( double ) m_initialNeuronsCount , 1.0 / ( double ) ( m_hiddenLayersCount - 1 ) )
: 1.0 ;
2026-08-22 00:25:52 -04:00
//--- 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.
2026-08-01 11:27:28 -04:00
int prevWidth = ( int ) ( m_historyBars * m_neuronsCount ) ;
bool result = true ;
for ( int i = 0 ; ( i < m_hiddenLayersCount & & result ) ; i + + )
{
int n = ( i = = 0 )
? m_initialNeuronsCount
: MathMax ( lastHidden , ( int ) MathRound ( m_initialNeuronsCount * MathPow ( taperRatio , ( double ) i ) ) ) ;
result = ( AddBatchNormStage ( Topology , prevWidth ) & & result ) ;
if ( ! result )
break ;
prevWidth = n ;
desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
{
delete Topology ;
return false ;
}
desc . count = n ;
desc . type = defNeuron ;
desc . activation = HiddenLayerActivation ( ) ;
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
result = ( Topology . Add ( desc ) & & result ) ;
}
if ( ! result )
{
delete Topology ;
return false ;
}
//--- Batch norm immediately before the head. This is the one placement that matters most: it is what
//--- keeps the logit spread from decaying as the weights below it shrink, and it is the precondition
//--- for ever running an UNBOUNDED head here (see the 2026-07-28 note on desc.activation below).
if ( ! AddBatchNormStage ( Topology , prevWidth ) )
{
delete Topology ;
return false ;
}
//--- Output Layer
desc = new CLayerDescription ( ) ;
if ( CheckPointer ( desc ) = = POINTER_INVALID )
{
delete Topology ;
return false ;
}
desc . count = m_outputNeuronsCount ;
desc . type = defNeuron ;
2026-08-22 00:25:52 -04:00
//--- 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).
2026-08-01 11:27:28 -04:00
desc . activation = OutputLayerActivation ( ) ;
desc . optimization = ( ENUM_OPTIMIZATION ) m_optimizationAlgo ;
if ( ! Topology . Add ( desc ) )
{
delete Topology ;
return false ;
}
if ( CheckPointer ( Net ) ! = POINTER_INVALID )
delete Net ;
Net = new CNet ( Topology ) ;
delete Topology ;
if ( CheckPointer ( Net ) = = POINTER_INVALID )
return false ;
2026-08-22 00:25:52 -04:00
//--- A fresh topology invalidates any existing shadow (see m_shadowNet's declaration comment) -
//--- its weights, if any, are shaped for the OLD Net and would either mismatch dimensionally or,
//--- worse, silently blend unrelated weight spaces if the shape happens to coincide.
2026-08-01 11:27:28 -04:00
if ( CheckPointer ( m_shadowNet ) ! = POINTER_INVALID )
{
delete m_shadowNet ;
m_shadowNet = NULL ;
}
//--- Let EnsureShadowNet() re-attempt the clone bootstrap once for this new topology (see the latch's
//--- declaration comment) - the old shadow, and any prior failed-bootstrap verdict, no longer apply.
m_shadowBootstrapAttempted = false ;
2026-08-22 00:25:52 -04:00
//--- A brand-new untrained net has NO online continual-learning history (see OnlineLearnStep):
//--- reset the watermark/guardrail/counters so a fresh start or a ResetWeights()-then-retrain
//--- never resumes from a superseded model's learned-up-to point or its stale rolling accuracy.
2026-08-01 11:27:28 -04:00
m_onlineLearnedUpToTime = 0 ;
m_onlineRollingAcc = -1.0 ;
m_onlineSamples = 0 ;
m_onlineBarsSincePersist = 0 ;
m_onlineBlendFrozen = false ;
return true ;
}
//+------------------------------------------------------------------+
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
//| Creates the OHLC + ZigZag indicators the feature builder reads. |
//| Called by InitNeuralNetwork(), not by the framework - the public |
//| InitIndicators() override is the framework entry point. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:57:54 -04:00
bool CExpertSignalAIBase : : InitFeatureIndicators ( CIndicators * indicators )
2026-08-01 11:27:28 -04:00
{
//--- Reset only the status label on (re-)init; deliberately do NOT PurgeChart() here so previously drawn
//--- signal arrows survive an EA re-init (recompile / param change / timeframe switch) instead of
//--- vanishing every time - see SIG_ARROW_PREFIX. Full cleanup still happens in the destructor.
ClearStatusLabel ( ) ;
2026-08-22 00:25:52 -04:00
//--- NOTE: LoadChartSignals() is deliberately NOT called here any more. The mismatch made the
//--- restore silently no-op on every restart from the moment the fingerprint was introduced. Same
//--- family as the fingerprint trap documented at BuildConfigFingerprint: anything keyed on
//--- m_fileName must run AFTER it is fully built.
2026-08-01 11:27:28 -04:00
if ( ! InitOpen ( indicators ) )
return false ;
if ( ! InitClose ( indicators ) )
return false ;
if ( ! InitLow ( indicators ) )
return false ;
if ( ! InitHigh ( indicators ) )
return false ;
//--- label source, always created unconditionally, same as the OHLC indicators above - see
//--- m_ADZigZag's declaration comment. Optionally ALSO read as an input feature (m_useSwingContext,
//--- below) using the same already-running indicator instance - no separate init needed for that.
if ( ! InitADZigZag ( indicators ) )
return false ;
m_neuronsCount = 4 ; // (close-open)/atr, (high-open)/atr, (low-open)/atr, bullish/bearish flag
if ( m_useVolumes )
{
feat(ai): widen the volume feature block from 1 value to 4
The block fed exactly one number: (v[i] - v[i-1]) / v[i-1]. That is the first difference,
and it cannot express three things that matter - the LEVEL relative to a baseline (two
dead bars and two frantic bars both read ~0 change), and the two volume-vs-range
interactions, where heavy participation that went NOWHERE (absorption) and heavy
participation that travelled (continuation) mean opposite things and currently collapse
onto the same value.
research/test_volume.py measures each candidate's mutual information with the triple-
barrier label across 3 instruments x 2 geometries, against a BLOCK-permutation null -
blocks sized to the barrier horizon, because adjacent labels share almost their entire
outcome window and a free shuffle yields a null so tight that everything looks
significant. Finite-sample MI bias (~7/n here) is reported alongside rather than
subtracted, since the permutation null already absorbs it.
Result: volLevel beats the shipped change ratio outright on 4 of 6 cells (EURUSD 2:3
+0.000118 excess at p=0.006, USDJPY 1:2 +0.000284 at p=0.002); absorption is the single
strongest reading anywhere in the sweep at EURUSD 1:2 (+0.000404, p=0.002) though it is
null on XAUUSD; vol x range clears on 4 of 6. The shipped change ratio is itself
significant on 5 of 6, so it stays.
Kept OUT: a session-relative z-score against the same hour-of-day's own recent history.
It was the weakest candidate - null on both EURUSD cells - and it is the only one needing
per-hour rolling bookkeeping in MQL5. Not worth the state for a reading that did not
survive its own null on the primary instrument.
Magnitudes, stated plainly because they are the point: the excess MI is ~2e-4 nats against
a label entropy near 1.05. That is under a tenth of one percent of the label's
uncertainty. It is real, it repeats across instruments, and it is nowhere near an edge -
this is worth having because it costs one 50-bar loop, not because it changes the answer.
Prior work stands: the whole single-series feature family measured at the noise floor.
m_neuronsCount is already in the fingerprint, so the width change re-keys existing caches
by itself, which is correct - the input vector genuinely changed shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:34:33 -04:00
// change ratio, level vs 50-bar baseline, absorption (range per unit volume), volume x range -
// see BufferTempDataCompute()'s matching block, and research/test_volume.py for the measurement
// that justified widening this from 1. m_neuronsCount is already in the config fingerprint, so
// this re-keys existing caches on its own: correct, the input vector genuinely changed shape.
m_neuronsCount + = 4 ;
2026-08-01 11:27:28 -04:00
if ( ! InitVolumes ( indicators ) )
return false ;
}
// Unconditional, same reasoning as m_ATR/m_ADZigZag below: m_Time.GetData() is read
// unconditionally elsewhere (label-eligibility gate, cache anchor, online-learning watermark,
// arrow timestamps) regardless of whether the cyclical time-of-day/day-of-week values are also
// opted into as an explicit feature via m_useTime - so the indicator itself must always exist.
if ( ! InitTime ( indicators ) )
return false ;
if ( m_useTime )
{
m_neuronsCount + = 6 ;
}
if ( m_useATR )
{
//already init in the base class
m_neuronsCount + + ;
}
if ( m_useMA )
{
if ( ! InitMA ( indicators ) )
return false ;
m_neuronsCount + = 5 ; // (open-MA)/atr, (high-MA)/atr, (low-MA)/atr, (close-MA)/atr, (MA-MA[1])/atr
}
if ( m_useRSI )
{
if ( ! InitRSI ( indicators ) )
return false ;
m_neuronsCount + + ; // RSI/100
}
if ( m_useMACD )
{
if ( ! InitMACDFeature ( indicators ) )
return false ;
m_neuronsCount + = 3 ; // main/atr, signal/atr, histogram/atr
}
if ( m_useIchimoku )
{
if ( ! InitIchimoku ( indicators ) )
return false ;
// (close-Tenkan)/atr, (close-Kijun)/atr, (Tenkan-Kijun)/atr, (close-SpanA)/atr, (close-SpanB)/atr,
// signed cloud thickness at this bar, signed PROJECTED cloud thickness, Chikou displacement
m_neuronsCount + = 8 ;
}
if ( m_useSwingContext )
m_neuronsCount + = 9 ; // 5 confirmed-pivot features (direction, distance-since-pivot, prior-leg magnitude, retracement ratio, bars-since-pivot) + 4 recent-context features (Donchian pos 20/50, 20-bar return, 20-bar SMA extension) - see BufferTempDataCompute()'s matching block
if ( m_useNews )
m_neuronsCount + = 2 ; // NewsRecency, NewsProximity - see BufferTempDataCompute()'s matching block
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
if ( m_useSpreadFeature )
m_neuronsCount + = 2 ; // spread/ATR (volatility-regime reading), spread change ratio
// - see BufferTempDataCompute()'s matching block
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
if ( m_useCrossAsset )
2026-08-11 21:07:52 -04:00
m_neuronsCount + = CROSSASSET_FEATURES ; // FX: base/quote strength + divergence; index: denom/risk-proxy strength
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
// divergence, cross-sectional dispersion - System\CrossAsset.mqh
2026-08-01 11:27:28 -04:00
if ( m_useADCumulativeDelta )
{
if ( ! InitADCumulativeDelta ( indicators ) )
return false ;
m_neuronsCount + = 6 ; // Pressure, CumulativeDelta, BullishPressure, BearishPressure, Absorption, Initiative
}
if ( m_useADShorteningOfThrust )
{
if ( ! InitADShorteningOfThrust ( indicators ) )
return false ;
m_neuronsCount + = 4 ; // SOT, SOTEffortRegime, SOTConfirmation, SOTPushRegime
}
if ( m_useADWyckoffEventStream )
{
if ( ! InitADWyckoffEventStream ( indicators ) )
return false ;
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
// 16 = 13 buffers - EventPrice (buffer 4, excluded; see BufferTempDataCompute()'s comment) with
// THREE of them split into a direction/magnitude pair each: EventCode, EventPhase and
// StructuralPhase are signed categoricals, so 13 readings now occupy 16 inputs (2026-08-09 audit,
// N1 - see the split in BufferTempDataCompute for why). No reading was added or dropped.
m_neuronsCount + = 16 ; // eventDir, eventStage, livePhaseDir, livePhaseMag, ZoneTop, ZoneBottom, structDir, structMag, CHoCHTrendToRange, CHoCHRangeToTrend, SlopeAccumulationBullish, SlopeAccumulationBearish, SlopeDistributionBullish, SlopeDistributionBearish, Reaccumulation, Redistribution
2026-08-01 11:27:28 -04:00
}
if ( m_useADWyckoffFailedStructure )
{
if ( ! InitADWyckoffFailedStructure ( indicators ) )
return false ;
m_neuronsCount + = 5 ; // Value, BullishStructuralFailure, BearishStructuralFailure, FailedAccumulation, FailedDistribution
}
if ( m_useADWyckoffSignificantBarInversion )
{
if ( ! InitADWyckoffSignificantBarInversion ( indicators ) )
return false ;
m_neuronsCount + = 5 ; // SignificantBarQuality, BullishSignificantBar, BearishSignificantBar, BullishControlFlip, BearishControlFlip
}
2026-08-16 13:39:00 -04:00
//--- ALT DATA (2026-08-16). Externally collected, publication-stamped features (COT positioning,
//--- VIX complex, macro) exported by research/altdata/export.py into
//--- Common\Files\Warrior_EA\AltData\{SYMBOL}_{TF}.csv - see System\AltData.mqh for the
2026-08-22 00:25:52 -04:00
//--- lookahead/degradation contracts.
2026-08-16 15:12:54 -04:00
if ( m_altDataEnabled )
{
string altPin = ReadAltDataPinFromCfg ( ) ;
if ( altPin ! = " " )
m_altData . SetPinnedNames ( altPin ) ;
m_altData . Load ( m_symbol . Name ( ) , ( ENUM_TIMEFRAMES ) m_period ) ; // logs its own outcome; absence is normal
m_altDataNamesPinned = ( altPin ! = " " ) ? altPin : m_altData . NamesCsv ( ) ; // fresh model: stamped by the first .cfg save
m_useAltData = ( m_altData . FeatureCount ( ) > 0 ) ;
if ( m_useAltData )
m_neuronsCount + = m_altData . FeatureCount ( ) ;
}
else
{
//--- Operator opt-out (EnableAltData=false): zero width, nothing pinned. On a model trained
//--- WITH alt features this shrinks neuronsCount, mismatches the .cfg compare and correctly
//--- starts fresh - stated in the input's comment rather than silently absorbed.
m_useAltData = false ;
m_altDataNamesPinned = " " ;
}
2026-08-01 11:27:28 -04:00
if ( ! FolderCreate ( m_folderPath , FILE_COMMON ) )
{
if ( GetLastError ( ) ! = 5010 ) // If the error is not because the folder already exists
{
Print ( " Failed to create folder: " + m_folderPath ) ;
}
else
{
ResetLastError ( ) ; // Reset the error code
}
}
return true ;
}
# endif