원본 프로젝트 mnbvc188199/Warrior_EA
| 작성자 | SHA1 | 메시지 | 날짜 | |
|---|---|---|---|---|
|
|
b4a704d309 |
feat(ai): triple-barrier labels replace exact-pivot ZigZag targets
The 31:1 class imbalance was self-inflicted by the TARGET, not a property of the market. Labelling only the exact bar where a ZigZag pivot confirms gave Buy 1164 / Sell 1164 / Neutral 35841, and every correction mechanism this codebase accumulated sits downstream of that one choice: the logit-adjusted loss and its range cap, the prior EMA, the +-3.0 output-bias seed, balanced-accuracy-then-precision selection with its coverage floor, the recall floor and its catch-22, the alternation gate, NMS, and the four oversampling designs that collapsed before them. The reference this engine is built on (references/neuronetworksbook.pdf ch. 3.1/3.3) also uses ZigZag, but targets the DIRECTION TO THE NEXT EXTREMUM on every bar - ~50/50 by construction, with no imbalance to correct at all. It never had this problem because it never asked "is this the pivot bar". Labels are now the triple barrier (Lopez de Prado ch. 3), using the EA's OWN SL_Mode/TP_Mode: does a trade opened at this bar's close reach its target before its stop, within a horizon. Buy = long resolves, Sell = short resolves, Neutral = neither. Consequences: - dir-precision in the era line stops being a proxy and becomes the win rate of the strategy under its own exit rules. - Expected balance ~25/25/50 at the shipped 1:3 (gambler's ruin), i.e. ~2:1 instead of 31:1. Measured and logged at the end of the prebuild. - Spread is charged on both legs, so it is a NET win rate. - Intrabar ambiguity resolves to the STOP. OHLC cannot order two touches inside one bar and the optimistic reading is how a backtested edge becomes a live loss. ZigZag stays as input features (EnableSwingContext) and now also supplies the vertical barrier: the horizon is the median confirmed leg length, snapped to a coarse ladder. Derived, not configured, and deliberately kept out of the filename fingerprint - a filename keyed on a measured quantity orphans a trained model the moment the measurement moves. Removed, because the premise died with the old target: - the alternation gate. Correct for pivot labels (a ZigZag cannot emit two same-type pivots in a row, so a repeat was provably a false fire), and wrong for barrier labels, which answer each bar independently. It also took its worst consequence with it: a one-sided model previously got ONE trade per backtest, a hard blocker on marketplace validation. - SignalClusterWindow now defaults off - it de-duplicated repeats that are now real trades. Kept as an opt-in display control. - LABEL_WINDOW_BARS, the pivot-widening pass, ConfirmedZigZagLabel. - the era-0 output-bias seed now needs a genuinely dominant class (0.70) rather than 0.40; at ~50% Neutral a +-3.0 seed is a distortion, not a correction. Also fixed, both found while wiring the above: 1. RefreshConvergedSignal sized its buffers from a date delta (Bars(sym, period, dtStudied, TimeCurrent())). dtStudied is a training watermark; in the tester it is loaded from a live-chart save AHEAD of the simulated date, so the interval inverted, Bars() returned ~0, and the buffer came out at exactly m_historyBars - deep enough for the OHLC window and far too shallow for the Donchian-50 / 20-bar-return / SMA extension behind it. Inference silently computed DIFFERENT features from the ones training learned on, live as well as in the tester. Now sized from what the feature builder actually needs. 2. The barrier horizon is resolved on the deployed path too. A deployed model never enters Train(), so it never reached the prebuild, and OnlineLearnStep reads the horizon as its confirmation delay - left at the fallback it would have backpropped bars whose barriers had not resolved. Silent lookahead in the one place that writes to a live model. SL_Mode/TP_Mode join the weights fingerprint: they define the labels now, so a model trained at 1:3 must never be silently reused at 1:1. This re-keys every pre-existing model by design - none were trained on this task. Inference census extended with the vote gate. LongCondition/ShortCondition open with a readiness check the refresh counters never see; in the tester it reduces to "the seeded _optcache.nnw must have LOADED", and if it did not, every vote is hard-zeroed while the model still answers Buy. The old three counters would have read that as "the model says Neutral" - false, and a completely different fix. This is the leading candidate for the zero-direction backtest and the census can now name it in one run. Both builds compile 0 errors / 0 warnings. Forces a full retrain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fa0455f399 |
diag: inference-path census, to explain zero-trade backtests
A backtest of the CONVERGED CONV model produced "Final directional result: 0.00000000" on every one of 1744 bars and therefore zero trades. Nothing in the log could separate the three candidate causes, and each needs a different fix: 1. RefreshLatestSignal never called (new-bar gate never fires) 2. called, but bailing at one of its two early returns 3. running fine, and the model genuinely answers Neutral every bar Counts all three plus the Buy/Sell/Neutral split, printed once at shutdown via StopTraining (which the tester reaches through OnDeinit). Three increments per bar against a full feedForward - not worth gating. Ruled out while writing this, so the next session does not re-derive it: - the alternation gate (m_lastNonNeutralSignal) is NOT the cause. It starts at Neutral, so a first Buy would still fire and show up as one non-zero direction. We saw zero. It IS still a live hazard for a one-sided model - CONV currently calls Buy:17% Sell:0%, and after the first Buy every later Buy is suppressed until a Sell that never comes - but it cannot explain an all-zero run. - shallow buffers do not hard-fail the feature builder: the swing-context Donchian loop breaks gracefully when it runs off loaded history. It does mean converged-path inference computes Donchian/return/SMA features over a TRUNCATED window versus training, which is a real train/inference skew worth its own fix, but it degrades features rather than zeroing them. Both builds 0/0. Diagnostic only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
251e9711dd |
diag: report |dW| alongside d|W| per layer, and drop two stale log claims
The per-era `dW/W` line measured the change in each layer's weight NORM. That statistic cannot separate "this layer only shrank under weight decay" from "this layer moved somewhere useful" - a rotation at constant norm and pure decay can print the same number. It matters right now: on SP500 H1 the LSTM layers print a near-constant ~1.05%/era that exactly equals their geometric norm decay over 318 eras (HYB lstm2 12.966 -> 0.755, monotone, never once up), while a sibling conv oscillates around a much slower drift. Norm-change can only hint at that. Now prints norm(d|W|% / |dW|%). Under decay alone the two are equal; any gradient component adds in quadrature to the second, so a learning layer shows the second clearly larger. Diagnostic only - no training behaviour changes, fingerprint untouched. Also removes two log lines that described machinery deleted in |
||
|
|
397b0eac1f |
refactor(ai): nine class-imbalance inputs down to two
The imbalance section offered nine controls for one job. Audited against the
code, five of them did not do what their names said at the shipped defaults:
AILogitPriorStrength DEAD - Inference.mqh's post-hoc prior early-returns
whenever the adjusted loss is on, which is default.
OversampleParity DEAD in training - Training.mqh gated the replay loop
on !useLogitAdjustedLoss (correctly, citing Buda et
al. 2018). Live only in the online-learning path.
EnableMinorityReplay DEAD as replay. It survived ONLY as a focal-gamma
damper - "replay minority bars through pass-2
oversampling" was a focal-loss switch.
ConstrainReplay DEAD as a cap; it only chose damper 0.125 vs 0.25.
UseStaticPrior An exact duplicate of FreezePriorCalibration - the two
were OR'd together in the single place either is read.
So they were not five mechanisms fighting; they were one mechanism plus eight
knobs that mostly described machinery that no longer ran. That is worse than
a real conflict, because the log agreed with the names: the label-cache line
printed "reps up to 28x (90% parity) (seeding era 0's class-balance
oversampling)" on every run, describing an oversampling pass that had been
switched off. It is fixed here too - it cost this session a wrong diagnosis.
The one genuine redundancy was focal loss, running at gamma*0.125 alongside
the adjusted loss: two corrections on the same axis, the exact stacking
failure this file already cited Buda et al. for in two other places, damped
by a replay flag whose replay path was itself dead. Removed rather than
re-tuned. The plateau ladder is unaffected - its escape is the learning-rate
warm restart; the gamma anneal beside it only ever stepped toward zero.
WHAT REMAINS is logit-adjusted loss (Menon et al. 2021) plus a prior freeze:
LogitAdjustTau 0 = off; replaces the separate EnableLogitAdjusted-
Loss boolean, since a strength dial where 0 already
means off does not need an on/off switch beside it.
FreezePriorCalibration unchanged.
It is the only one of the six corrections with a consistency guarantee, and
it is consistent for exactly the balanced-error metric checkpoint selection
already ranks on - so the loss and the deploy decision optimize one thing.
The online continual-learning path keeps its own alpha-balanced focal weight,
now as constants pinned to the removed inputs' shipped defaults, so its
behaviour is unchanged. It legitimately needs its own correction:
ApplyLogitAdjustment() only runs inside a training run, so a deployed model
that was reloaded carries no logit offsets and would otherwise stream 31:1
data into itself uncorrected.
The weights-filename fingerprint is BYTE-IDENTICAL. The focal slot was a
double fed to a %d conversion and had always emitted a literal 0; the |MR:
segment is written as the constant its shipped defaults produced. Dropping
either would have re-keyed every model and forced a from-scratch retrain of
the one topology currently converged and trading.
Also removed as orphans: FOCAL_GAMMA_PRESET, MAX_OVERSAMPLE_REPLICAS,
OVERSAMPLE_PARITY_FRACTION, PLATEAU_GAMMA_STEP, and the now-unreachable
"neutralized by prior correction" diagnostic.
Both builds compile 0 errors, 0 warnings. No retrain forced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
18f63c7a52 |
feat(ai): report per-layer weight movement each era
Adds "dW/W dense1:0.412(0.31%) conv1:0.088(0.000%) ..." to the era line: each layer's weight L2 norm and its relative change since the previous era. Why: a frozen stage and a badly-suited architecture look identical from the outside. Both give a flat metric and a retreat to the majority class, and neither the loss, the accuracy nor the per-class recall can tell them apart. This session cost two full retrain cycles guessing between them - a forget- gate bias (a real bug, measured, but not the cause of the observed failure) and a conv receptive field (which turned out to be a regression, not a fix). A layer sitting at ~0.000% era after era while its neighbours move is receiving no gradient, and no amount of retraining or hyperparameter work will change that. A net where every layer moves and the output still collapses is a genuine architecture or objective problem. The distinction is one glance at the log instead of a redeploy-and-wait cycle per hypothesis. Costs one host-side buffer read per layer per era, off the training path. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
34d6aa42a4 |
feat(ai): real conv receptive field + the reference's channel pool
CONV's convolution used window = step = one bar, which is a per-bar projection - a 1x1 conv with a temporal receptive field of ONE BAR. It never mixed information across time, so "convolutional" described the layer type and nothing about what it computed. Same finding that sank HYBRID's LSTM. Pooling was removed on 2026-07-29 for being misconfigured against the conv output's memory layout. That removal was right; leaving the conv at a one-bar window was not. The two belong together: the NeuroNet_DNG reference (references\MQL5\Experts\EDL\Trajectory.mqh layers 2-5, kernels byte-identical to ours) pairs conv(window=2, step=1, window_out=4) with pool(window=4, step=4), and the pool only earns its place because a conv with a real receptive field sits above it. The input is bar-major (BufferTempData appends m_neuronsCount contiguous features per bar), so a flat window of k*m_neuronsCount spans exactly k bars - the receptive field needed NO kernel change. The conv output is position-major, so window == step == window_out is a clean max-over-channels, which is what the reference does and what the existing pool kernels already implement correctly. New chain at H1 defaults (420 = 20 bars x 21): conv1 w=42 s=21 out=8 -> 19 pos x 8 = 152 pool w=8 s=8 -> 19 conv2 w=2 s=1 out=8 -> 18 pos x 8 = 144 (effective field: 3 bars) We deliberately stop before the reference's SECOND pool: a channel pool emits one scalar per position, so a trailing pool would hand the dense stack 18 values and force it to fan out 18 -> 64. That is a bottleneck below every learnable layer - the same class of mistake the 2026-07-29 removal was about. Fixes a latent sizing bug this exposed: CNet's conv/pool position cursor tracked sliding POSITIONS, but a conv's real width is units_count * window_out. Any pool stacked on a conv would therefore have sized against a width window_out times too small and silently built the wrong shape. Both branches now read the built layer's actual Neurons(), which is what the batch-norm branch already did for the same reason. Also closes the architecture-pinning trap: a .nnw persists the window each conv was built with, so an existing CONV/HYBRID model would have loaded cleanly and gone on training under the OLD architecture. The conv weight tensor is (window+1)*window_out, so this cannot be repaired in place - EnforceTopologyContract now detects it, reports both shapes, and retrains. Conv chain shape is derived in one place (ConvReceptiveFieldBars / ConvFirstStagePositions / HasSecondConvStage / ConvOutputPositions / ConvOutputWidth) and consumed by AddConvStage, LstmFanIn and the startup config line, so what is built and what is logged cannot drift. Both builds compile 0 errors, 0 warnings. Forces a CONV and HYBRID retrain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4eae763849 |
fix(ai): report the metric actually compared; surface the derived front-end
The plateau/regression line printed balancedOosEra as the current value while comparing against m_bestBalancedOos, which has held the SELECTION score since |
||
|
|
ff06583680 |
feat(ui): drop the config tag from the plain-language panels
"Hybrid 3L [HYB-9369] - learning (era 4, 12%)" leads with a fingerprint hash that means nothing to an owner. The tag earns its place in the journal and the State\ folders, where telling one chart's model files from another's is the whole point - but the default panel is the commercial surface and should not open with a debug token. New DisplayName() strips the bracketed suffix; the two plain-language panels (training and live/idle) use it. Logs, the VerboseMode panels and the auto-tune line keep the full ID, so nothing needed for diagnosis is lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1039ad936f |
feat(ai): measure precision per confidence tier; fix stale metric labels
Two things the 2026-07-30 run exposed.
1. Every user-facing message still called the selection metric "balanced
accuracy". It has ranked on directional precision since
|
||
|
|
ce90fc74b6 |
fix(ai): discount selection precision by coverage shortfall
The 2026-07-30 run caught a bug in the precision-led selection metric within 8 eras. HYBRID made exactly ONE directional call in era 7, got it right, scored 100% precision, and locked that in as best-ever. Nothing can beat 100%, so the checkpoint froze on a single sample and the run could only burn to the era cap deploying it. The coverage floor already existed and already blocked that era from being DEPLOYABLE - but the ranking ignored coverage entirely whenever no era had qualified yet, which is precisely the phase where the ranking is the only thing steering the run. Precision is now discounted by coverage/floor, capped at 1.0. Continuous rather than a threshold: an era at half the floor scores half its precision, so coverage and precision both improve rank and neither can be traded away. Above the floor the credit saturates, so ranking among genuinely deployable eras is unchanged pure precision. Also: the startup config line printed "tau 1.00" while every chart was actually running the capped 0.35 - the effective value depends on the measured class priors and is not knowable at init. Now reads "1.00 requested"; ApplyLogitAdjustment still logs the real figure. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
45b35b3d1d |
feat(nn): derive dense depth, train on all history, pin the shape in .cfg
Completes the derived-topology work. Three inputs removed. AIType loses its depth suffix - AI_MLP/AI_CONV/AI_LSTM/AI_HYBRID, five entries instead of eight. Depth is now derived from the two endpoints the taper already has to connect (derived first-layer width, output-tied final width) at a 2x per-layer compression target, clamped [2..5]. Asking a user to pick a layer count while the code derives the widths those layers taper between was asking for half a decision: at 64 units tapering to 12, four layers compress by 1.4x per step and five by 1.3x, so the extra depth bought no abstraction. On the shipping H1/10y default the derivation lands on 3 layers - the depth that actually won Run 2. StudyPeriods removed. There is no case for training on less data than the broker provides at a ~6% directional base rate; the honest generalization read comes from the OOS holdout, not from withholding history. Training now starts at the earliest available bar, floored by MinTrainYear, which answers a different question (excluding dubious pre-history) and stays. That required closing the hazard the old code documented: the capacity budget now MEASURES the symbol's real bar count, and a topology derived from a measurement would widen as history downloads. Both ends are now pinned. Every derived value left the weights-filename fingerprint - keying a filename on a measured quantity means the EA looks for a file that does not exist, starts from era 0 and orphans a trained model, silently, because a missing cache is the normal first-run state. The shape lives in the .cfg instead, where LoadAndCompare now ADOPTS the four derived fields rather than diffing them; a mismatch there would discard a fully-trained model over nothing the user did. Two fields appended to the .cfg for the conv/LSTM stages, length-guarded on read because FileReadInteger past EOF returns 0 with no error. ForceHiddenLayers, a compile-time constant like DebuggingMode, pins depth for diagnostic comparisons. It joins the fingerprint only when non-zero, so forced depths get their own files - sequential comparisons only, not simultaneous from one .ex5. Derived shape, H1/10y defaults (21 features x 20 bars): first layer 64, 3 dense, 8 conv filters, 16 LSTM units. The LSTM block halves from ~58k to ~28k weights. Both builds compile 0 errors, 0 warnings. Re-keys existing models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ebf2e73667 |
fix(ui): unique chart tag, product-grade panel, responsive under load
Three separate reports from one deploy. 1. CONV, LSTM and HYBRID all came back tagged [4109]. The weights fingerprint omits the topology type on purpose - the file path already separates it (State\CONV\ vs State\LSTM\ vs State\HYB\) and hashing a value that is constant within a folder buys nothing while re-keying every trained model into a forced retrain. So the files were never at risk, but the tag could not do its one job. Prefixing the short id makes it unique on the display side only; the hex half still greps straight to the .nnw inside the folder the prefix names. 2. The default panel read like a training console. Six lines down to three, each answering a question an owner actually has. The deploy internals (best score, eras-since-best, ladder stage) were developer diagnostics describing a recall floor that no longer decides anything, and were already in the era-end journal line. In-sample accuracy left the panel too: it grades the model on bars it trained on, so it always flatters, and showing it beside the honest number invites reading the wrong one. New compile-time DebuggingMode constant - deliberately not an input - carries the IS/OOS pair and the resolved model path into the journal instead. No extra Inputs row, no extra Market description line, no user-reachable firehose. 3. Panel drag and buttons stuttered under training load, exactly as the 2026-07-26 note raising the chunk budget to 200ms warned they might. Backed off to the documented 120ms - worst-case click latency is that budget - and the derived topology (~292k weights to ~29k) makes the throughput this costs far cheaper than when that note was written. Also halved the panel redraw rate to 2.5 Hz: ChartRedraw repaints the whole chart, so its cost scales with accumulated arrows, and 5 Hz was the larger half of the stutter. Era-end still force-refreshes. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a142749a87 |
feat(ai): rank checkpoints on directional precision, not balanced accuracy
Balanced accuracy is maximized by exactly the model this system must never
deploy. Measured frontier at fixed signal strength, base rate 6.1%:
tau 0.00 -> calls 0.2% of bars at 27.3% precision, balanced 34.0%
tau 0.35 -> calls 2.0% of bars at 15.5% precision, balanced 36.3%
tau 1.00 -> calls 49.6% of bars at 6.4% precision, balanced 53.5%
It rises monotonically as the model calls MORE and is right LESS, because
two of its three terms are directional recalls that a call-everything model
drives to ~95%, while the Neutral term it sacrifices counts for only a
third. The 2026-07-29 run landed exactly there: balanced 58-64% while
calling a direction on ~100% of bars at a 5-7% win rate against a ~6% base
rate. Only the per-class recall floor stopped those deploying - a guard
doing the job the objective should have been doing - and that same guard
also rejected the genuinely useful sparse-but-precise checkpoints.
Ranking is now DIRECTIONAL PRECISION: of the bars called Buy or Sell, how
many were right. That is what a trading edge is. Two anti-degenerate floors
bracket it, since precision alone is trivially maximized by calling almost
nothing: coverage must reach a fraction of the true directional base rate
(derived, not configured - it adapts to any symbol/timeframe/label rule),
and precision must at least beat that base rate.
Against the same frontier the deploy order inverts from
tau 1.00 > 0.50 > 0.35 > 0.15 (old, worst model first)
to
tau 0.35 > 0.50 > 1.00 (new; 0.00/0.15 rejected on coverage)
Balanced accuracy is kept in the log as a diagnostic and marked as such, so
a run where the two disagree - the signature of an over-caller - is visible
at a glance. MinRecall no longer decides what ships; it now only drives the
diagnostic recall line and is a candidate for removal.
Both builds compile 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0e5f1bb2f6 |
fix(ai): cap logit-adjustment strength to the head's usable logit range
tau=1.0 inverted the collapse instead of curing it. The head is SIGMOID, so each output is bounded to [0,1] and the widest logit gap the net can express between two classes is CLASS_LOGIT_SCALE * (1-0) = 6. The offsets are tau*log(prior_c), whose spread on this 30:1 imbalance is 3.42 - so tau=1.0 spent 57% of the ENTIRE expressible range on the prior correction. The network did the only thing available to it: saturate Buy/Sell outputs to 1.0 to overcome a -3.42 training handicap. The offsets are absent at inference, so that surplus made every bar directional. Measured across all five still-training charts: Neutral recall 0%, directional calls on ~100% of bars, win rate 5-7% against a ~6% base rate - no information whatsoever - while balanced accuracy read a flattering 58-64% because two of its three terms sat near 95%. OOS accuracy 6%. Menon et al. assume an unbounded logit head where a 3.42 shift is negligible against the reachable range. It is not negligible here, so the strength is now expressed RELATIVE to the range actually available: tau_eff = min(tau_cfg, LOGIT_ADJUST_MAX_RANGE_FRACTION * SCALE / spread) At 20% that gives tau 0.35 on this data. Deliberately a fraction rather than a tau ceiling: it stays correct if CLASS_LOGIT_SCALE changes, if the head becomes unbounded, or on any symbol whose imbalance differs. The input remains effective below the cap, so dialling it down needs no rebuild. Simulated at a signal strength where the task is genuinely learnable, the precision/recall frontier is monotone: tau 1.0 -> 49.6% call rate at 6.4% precision (base rate 6.1%, i.e. worthless); tau 0.35 -> 2.0% at 15.5%; tau 0.15 -> 0.2% at 33.3%. The capped value lands in the same regime the pre-logit-adjustment run occupied (1-6% of bars at 20-35% win rate). Also logs the measured priors, the spread, and whether the cap bound. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |