Commit graph Warrior_EA/Expert/AIBase
Author SHA1 Message Date
AnimateDread
f2ec1edf84 feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior
Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add
tau*log(prior_c) to each class logit inside the training gradient. Softmax
CE on adjusted logits is consistent for BALANCED error - the metric
checkpoint selection already ranks on - so the loss and the deploy decision
finally optimize the same thing.

The engine already computed a true softmax + categorical-CE gradient and
wrote it over the per-neuron sigmoid delta, so this is an offset added to
three logits in the two places that gradient is built (backProp scalar path
and backPropOCL). No backend, kernel or DLL change; the forward pass and
every inference path are untouched, which is the point - the network learns
to absorb the offset, so its raw argmax becomes the balanced-optimal
decision with nothing applied at inference.

Replaces rather than stacks. Minority replay is disabled while this is on,
and the post-hoc inference prior is forced off. Stacking is not a
theoretical worry: simulated on the measured 1118/1119/34298 distribution
in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced,
Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance -
and BOTH together score 45.4% with Neutral recall at 0%, worse than either
alone. Buda et al. 2018 predicts exactly that.

Motivation from the six-chart run: every topology took one direction to
~50% recall and abandoned the other, the direction chosen arbitrarily (the
batch-norm control went Buy 1% / Sell 42%, the inverse of the other five).
One era in 1,301 cleared the per-class recall floor.

Fingerprinted conditionally, so the converged 60.7% models on disk keep
their filenames and stay loadable as the fallback.

Both builds compile 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
AnimateDread
65dc1bddb4 fix(ai): freeze batch-norm statistics when comparing two forward passes
With normalization enabled a forward pass is not a pure function of its
input - it also advances the running mean/variance. ValidateCpuInference
compares the live backend net against a throwaway pure-MQL5 clone loaded
from the just-saved .nnw, so its own reference pass left the live model one
EMA step ahead of the file the clone reads. The check would then have been
measuring its own side effect, and a marginal result decides whether
buyers' backtests are allowed to run DLL-free.

Adds CNet::SetBatchNormFrozen / CNeuronBatchNormOCL::SetStatsFrozen -
classic batch-norm inference semantics, statistics used but not updated -
and freezes both sides for the duration of the comparison. Not persisted:
it is a transient evaluation mode, not model state. Default stays
adaptive, which is what the rest of the system (online continual learning)
is built around.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:37:50 -04:00
AnimateDread
1aa7df9096 fix: stop a .nnw from pinning a superseded architecture
A .nnw persists the ARCHITECTURE, not just the weights: Save writes
(int)activation per neuron and Load reads it straight back. The activation
chosen in BuildFreshTopology() therefore only ever reached a brand-new
topology - every reload restored the file's value and the next save wrote it
back out, so a wrong value could never heal while the source read as though
it were already fixed.

That is how five models kept training with an unbounded NONE classification
head for a full day after the 07-28 revert to SIGMOID. Confirmed by parsing
the binaries: 848cb42c.nnw / 2e754b43.nnw carry `act=NONE` on the 3-neuron
output layer, while a genuinely reset model of the same config carries
act=SIGMOID. In the log it showed as negative "OOS raw out" values -
impossible under sigmoid - escalating to a 4.14e13 logit spread with all
three classes numerically identical (input-independent output) and balanced
accuracy pinned on the 33.3% one-class floor.

- OutputLayerActivation() is now the single source of truth, called by both
  BuildFreshTopology() and the new load-time repair, so the two can no
  longer diverge the way a duplicated literal did.
- CNet::EnforceOutputActivation() re-asserts it after Load and reports the
  stale value; CExpertSignalAIBase::EnforceTopologyContract() logs the
  repair loudly, since weights learned under the old head may not be worth
  keeping even once the head is corrected.
- Hidden layers are deliberately left alone: they legitimately differ per
  stage (PRELU dense/conv, NONE pool, TANH LSTM).

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:00:40 -04:00
AnimateDread
4cb888e1b1 fix: clear stale signal arrows when a fresh model starts at era 0
Arrow cleanup existed on two paths - the panel's reset-weights, and the
topology-mismatch discard - but both are gated on there being a saved .nnw to
delete. The third case had no cleanup at all: a fresh topology at era 0 with no
weights behind it, which is what a changed config produces. A new fingerprint
makes a new m_fileName, so the previous model's files are not "discarded", they
are simply not this model's files, and nothing ever cleared the chart.

That is not cosmetic. Arrows outlive the model that drew them twice over:

  1. The chart objects live in the CHART, not the sidecar, so they survive a
     remove/re-add, a recompile, a restart and a fresh deploy no matter what
     happens to any file on disk.
  2. SaveChartSignals() rebuilds the sidecar by SCANNING the chart for
     SIG_ARROW_PREFIX objects. So the first save of the fresh run adopts the
     dead model's calls and writes them out under the NEW model's filename -
     laundering them into the new model's history where nothing can separate
     them afterwards.

Extracted the duplicated cleanup into ClearPersistedChartSignals(reason) - it
cancels the deferred restore queue, deletes m_fileName + ".arrows", clears the
namespaced chart objects and logs why - and called it from all three paths.

The call sits at the BuildFreshTopology() call site, not inside it: the genetic
tuner rebuilds a throwaway topology per candidate (AutoTune.mqh) and must never
touch the chart. All three sites run after m_fileName has its config fingerprint
appended, so they target the right sidecar.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:13:01 -04:00
AnimateDread
cc625c827e fix(training): escape the recall-gate catch-22 that let runs decay unchecked
Evidence (MQL5\Logs, SP500 H1, 2026-07-29):

  Perceptron  era  61  Buy 32% Sell 27% Neut 94%  bal 51%
  LSTM        era 160  Buy 16% Sell 11% Neut 98%  bal 42%  (peaked 49% @ era 44)
  Hybrid      era 179  Buy  5% Sell  2% Neut 99%  bal 35%  (peaked 41%)
  CONV        era 228  Buy  2% Sell  4% Neut 99%  bal 35%  (peaked 40% @ era 122)

Every model peaks early then decays monotonically toward Neutral, and nothing
stops it: the restore-best-weights + decay-eta handler is gated on
m_bestPassedRecall, which stays false forever when no checkpoint ever clears the
per-class floor. CONV ran 228 eras with eta pinned at its 0.000300 start. The
plateau ladder cannot end such a run either (stage 3 refuses to deploy without a
recall pass, so it resets ~27 times), making it a 1000-era one-way trip.

The gate's own justification had expired. It was written when the pre-pass
tiebreak was blended-accuracy-only, where "best" really did mean "called Neutral
most confidently". The balanced-selection change replaced that with
`balancedOosEra > m_bestBalancedOos` plus an isFullyCollapsedEra exclusion, so a
Neutral-only era now scores ~33% - the FLOOR of the balanced metric - and cannot
anchor the checkpoint at all. Pre-pass "best" now means "most class-balanced so
far", which is worth defending; and isWorseEra is itself a balanced-accuracy
regression, so it cannot fire merely for trading Neutral calls for Buy/Sell.

The original concern still holds while the best-so-far IS near-collapse, so the
escape is margin-guarded: defend the checkpoint only once balanced accuracy sits
more than BALANCED_WORTH_DEFENDING_MARGIN_PCT (5pp) above the one-class floor of
100/3. Against the run above that engages for all three stuck topologies
(42.3/41.3/50.0 vs a 38.3 threshold) while a genuinely collapsed run still
explores freely.

Two inputs restored to the regime that actually produced a deploy:

- MinRecall 60 -> 40. The one successful auto-deploy in the logs (Hybrid, 28th
  00:50, best balanced 66.0%) ran against a 40% floor. 60 has never been shown
  reachable here - a floor above what the config can reach is the same "target
  set too high" failure the surrounding comment already warns about.

- OversampleParity 60 -> 90. 60 overcorrected. Runs now START Neutral-dominant
  (Buy 0-11% recall at era 1) and call Buy/Sell on 0-4% of bars against a ~6%
  true base rate - under-calling, with no headroom to converge down from. The
  deploying run began at Buy 90% / Sell 36%, 24% of bars called, and settled into
  the floor from above. Raw over-calling is the intended starting condition; live
  calls are base-rate-calibrated by AILogitPriorStrength, which is why the input's
  own note says to judge over-calling by live-fired precision, not raw counts.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:51:08 -04:00
AnimateDread
41341f44c1 fix(panel): show the metric that actually gates deployment
The simple panel showed "Buy/Sell accuracy: IS x% OOS y%" from m_cumIs*/
m_cumOos*, which are monotonic lifetime counters: never reset per era (only on
reset-weights) and restored from .stats across restarts. So the number is the
average over EVERY era ever trained. At era 217 one more era moves it by well
under a percent - it reads flat whether training is healthy or dead, and a model
that started badly and has since recovered still shows low.

That is the only number the non-verbose panel offered, so there was no way to
tell "still improving" from "stuck" while watching four charts.

Added the actual gate. The plateau ladder only auto-deploys a checkpoint that
cleared m_minDirectionalRecallPct on EVERY class (Buy AND Sell AND Neutral,
default MinRecall=60%). If nothing ever clears it, stage 3 deliberately refuses
to deploy, resets the ladder and keeps training to the era cap - correct
anti-collapse behaviour, but externally indistinguishable from being stuck.

Panel now shows:
  - era against the cap, not just the era number
  - the accuracy line explicitly labelled "(lifetime avg)"
  - best balanced accuracy vs the per-class floor it must clear
  - eras since best + ladder stage, so plateau escapes are visible

Display only - no training, selection or convergence logic touched.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:15:45 -04:00
AnimateDread
26ac479bae docs: refactor + audit notes; fix malformed comment banner
REFACTOR_NOTES.md records what was found, what was changed, what was
deliberately left alone, and the one investigation that is still open (the
MLP CPU-DLL slowdown, with the parameter counts that rule out my earlier
"largest weight matrix" explanation).

Also restores the missing opening rule on ReInitADIndicators' comment banner.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:46:01 -04:00
AnimateDread
2de93539d4 refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.

Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:

  Training.mqh        1607  era loop, plateau ladder, checkpoint select, deploy
  Features.mqh        1093  indicator creation + per-bar input feature vector
  ChartUI.mqh          634  arrows, arrow persistence, status panel, cleanup
  Persistence.mqh      492  .stats/.cfg sidecars, CPU-inference validation, copy
  OnlineLearning.mqh   461  live continual learning, EMA shadow, OOS simulator
  Labels.mqh           309  ZigZag pivot labels, async label-cache prebuild
  AutoTune.mqh         275  genetic tuner (population, crossover, halving)
  Inference.mqh        235  softmax, prior calibration, class priors

  ExpertSignalAIBase.mqh  8216 -> 3131 (declaration + topology build only)

This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.

Compiles 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00