提交图

298笔提交

作者 SHA1 备注 提交日期
AnimateDread
a77ff64b13 fix(deinit): a full model write was running ahead of the cheap cleanup
"Abnormal termination" is back, and this time it is not the arrows. The
timing names the culprit exactly:

  16:02:31.547  OnDeinit: shutting down
  16:02:36.003  Abnormal termination          <- 4.46 s, MetaTrader gave up
  16:02:36.226  chart signals - persisted     <- cleanup finished 0.2 s LATE

OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining()
finalises an in-flight run, and FinalizeTrainRun() restores the best
checkpoint and then persists it - a full ~1MB model write per signal. So
the expensive step ran ahead of the cheap bounded one, which is precisely
the inversion the shutdown ordering exists to prevent. The previous fix
put PersistWeightsOnShutdown last and missed that StopTraining smuggles a
second save in at the front.

Two changes:

Cleanup now runs FIRST, then StopTraining, then the weight save. The
visible teardown is cheap and bounded, so it always completes even when
everything after it is killed.

And the deploy-persist inside FinalizeTrainRun is suppressed during
shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint
is already the live net by that line, and PersistWeightsOnShutdown writes
exactly those weights moments later. The old path wrote the same model
twice per signal - eight full writes across four charts - for no benefit.
A user-pressed Stop still persists immediately, because nothing else
would.

Compiles 0 errors / 0 warnings. Build tag deinit-order-v2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
AnimateDread
7d038df749 research: export the feature matrix and a raw OHLCV grid for offline work
The bottleneck on this project has never been the modelling - it is that
every hypothesis costs a compile, a deploy, an attach and a log read, and
answers exactly one question. Days have gone into questions that are
seconds of arithmetic once the data is in hand.

Adds a RESEARCH-ONLY build, gated behind WARRIOR_EXPORT_FEATURES and
never compiled into a shipped binary, which writes two things to
Common\Files\Warrior_EA\Research\ and then does nothing at all:

  <symbol>_<tf>_features.csv - one row per bar: index, time, OHLC, ATR,
  and the m_neuronsCount feature values. Exactly what the network sees.
  The raw bars ride along on purpose: with OHLC and ATR offline, every
  barrier geometry, horizon and in-trade target is recomputable without
  MetaTrader in the loop.

  <symbol>_<tf>_rates.csv - raw OHLCV across a grid of 8 symbols x 5
  timeframes. The 26 engineered features only exist for the attached
  chart (indicator handles bind to PERIOD_CURRENT); raw rates do not, so
  ONE attach yields the whole research grid. The bar time also makes
  session/hour/day-of-week derivable - the only inputs in play that are
  not a transform of the same OHLCV series.

Safety, because this binary gets attached to a chart on a LIVE ACCOUNT to
reach real history:
  - OnTick returns immediately, so Expert.OnTick() - the entire trading
    path - is unreachable regardless of the AlgoTrading toggle, the
    signal state or the inputs. Structurally incapable of sending an
    order, not merely unlikely to.
  - No config lock. It never trains and never saves a model, so it has
    nothing to protect against a concurrent chart - and taking the lock
    would make it refuse to start exactly when the config it wants to
    read is already open, which is when it is most useful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:49:57 -04:00
AnimateDread
004f2a04f7 fix(diag): the symbol sweep was measuring its own sampling, not the market
Twelve cells came back with higher-timeframe "signal" 5-9x anything on
H1, at p=0.005. It was an artifact, and the sweep's own columns gave it
away: excess tracked the sampling STRIDE almost monotonically, and the
three D1 cells - stride collapsed to 1-5 bars against a 128-bar horizon,
i.e. ~99% window overlap - were the three highest. Three flaws, all the
same family: comparing numbers without the spread that belongs to them.

1. THE NULL ASSUMED INDEPENDENCE THE LABELS DO NOT HAVE. Triple-barrier
labels overlap; two rows less than one horizon apart share most of their
outcome window. A free Fisher-Yates shuffle destroys that dependence
along with the association, making the null far narrower than the truth
and handing out significance that isn't there - Lopez de Prado ch. 4
arriving through the back door of the significance test. Now permutes
contiguous BLOCKS of at least one horizon, so the null keeps the
autocorrelation and the p-value means what it says. It degrades honestly:
severe overlap leaves few blocks, the null widens, nothing reaches
significance. The block count is now printed, because THAT - not the row
count - is the sample size a p-value rests on, and a warning fires under
30 blocks so "not significant" is not misread as "no signal" when it
means "not enough independent history to tell".

2. THE POSITIVE CONTROL'S STRENGTH DEPENDED ON THE DATASET. It paired
each row's label with the NEXT SAMPLE ROW's, whose distance is the
stride - so on M5, where stride ran 160-717 bars against a 128-bar
horizon, it was pairing two windows that never overlap. All three M5
cells duly reported a FAILED estimator and voided their own results with
nothing wrong. A control whose strength varies with the cell cannot
certify the cell. Now pinned to a quarter of the horizon, where ~75%
overlap is guaranteed by construction.

3. THE LOOKAHEAD VERDICT HAD NO MARGIN. It flagged 7 of 12 cells on gaps
of 0.00008-0.00040 nats against a measured null sd of ~0.00030 - noise,
every one. Now requires 3 sd, the same discipline the deploy floor
applies to precision.

Compiles 0 errors / 0 warnings, standard and Market. Build tag
blockperm-v1. Supersedes every number from the sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:11:40 -04:00
AnimateDread
168422ff7a fix(labels): the 128-bar horizon ceiling was truncating the shipped label
The corrected geometry scan exposed something bigger than the geometry
question it was asked. Every pairing from 2:6 upward came back CLAMPED -
including 2:6, the SHIPPED configuration.

First-passage time for a driftless walk leaving [-m,+k] goes as m*k, and
the measured swing median here is ~12 bars at m*k=1, so 2:6 wants ~144
bars and 3:10 wants ~360. The ladder stopped at 128. A clamped label
stops meaning "does the target come before the stop" and quietly becomes
"...within 128 bars", while the deployed EA holds until SL or TP with no
bar limit. So the target the models have been trained on all along was
not the strategy the EA executes, and the trades it silently reclassified
as Neutral were the SLOW WINNERS - precisely the ones a 1:3 barrier
exists to capture. Timeout share stayed ~0% throughout, which is why this
never showed up: the truncation lands in Neutral, not in the timeout
counter that was watching for it.

Ladder extended to 384 (12..128, 192, 256, 384) so every selectable
geometry gets an honest horizon. Cost is one embargo of at most 384 bars
out of ~38k.

Second fix, same class of error as the H(Y) one: the scan's "best
eligible" was 2:2, a 1:1 barrier, against a shipped Min_Risk_Reward_Ratio
of 1:2. Training four topologies on that target would have produced a
model whose every setup is rejected at the door - the exact failure
behind four consecutive Market rejections for "no trading operations".
Sub-minRR geometries are now ineligible and marked [<minRR], printed
rather than hidden.

Also drops the dense-depth tag from the display name ("Perceptron 3L" ->
"Perceptron"). Depth is derived, so it names nothing a user chose; the
config tag [PAI-0be2] already disambiguates concurrent charts and does it
for every input rather than one. Full topology still logged by "config -".

Compiles 0 errors / 0 warnings, standard and Market. Build tag
horizon-384-v1. Changes the LABEL for every geometry, so the next scan
supersedes the previous numbers - and a retrain is required before any
model trained under the truncated target means anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:54:45 -04:00
AnimateDread
40af4a4b5b fix(labels): the geometry scan rewarded the labels it should reject
First run named 3:10 on all four charts, at 2.3x the configured 2:6. That
answer was wrong and the fault was the ranking statistic.

3:10 wants a horizon of ~swingMedian*30 (~320 bars) and gets
BARRIER_HORIZON_MAX. Clamped, most trades never resolve, the unresolved
remainder all lands in Neutral, and H(Y) collapses. The old statistic
divided the excess BY H(Y) - so a collapsing denominator made the most
degenerate label look like the most predictable one. Every geometry from
2:6 upward was already showing the clamped h128, and the two widest
scored highest, which is the fingerprint of the artefact rather than of
signal.

Two fixes:

Rank on the raw excess in nats. Subtracting each geometry's OWN measured
null already removes the class-balance bias, which is the only thing the
normalisation was ever needed for.

Disqualify clamped geometries outright rather than ranking them down. The
deployed EA holds until SL or TP with no bar limit, so a truncated label
trains the model on a question the strategy never asks. They are still
printed, marked '!', so the disqualification is visible instead of a
silent omission - and the scan now says so explicitly when nothing
eligible is left, because "the limit is the feature set, not the target"
is itself the finding in that case.

The scan also reports each geometry's directional share and timeout share
now. A label nobody can trade is not a candidate however well it scores,
and that has to be visible in the same line as the score.

Compiles 0 errors / 0 warnings. Build tag geometry-scan-v2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:46:25 -04:00
AnimateDread
f97ab9f1d6 feat(labels): measure which barrier is predictable at entry, don't guess
The alignment scan settled the shape of the problem: 4.7x more is
knowable 5 bars into a 128-bar window than at the entry the model
actually trades. A 6xATR target reached over 128 bars is decided
overwhelmingly by what happens DURING the window, so whatever the entry
state knows is buried under 128 bars of later noise. That is a property
of the TARGET, and it is why four different architectures all landed on
precision exactly equal to the base rate - no topology can undo it.

So measure the target. For each SL/TP pairing a user can actually select,
relabel the same sampled bars and score how much the SAME features say
about THAT outcome at entry. Seconds, no training, no topology, and it
runs on the diagnostic path that already exists.

Ranked on excess over its OWN null as a share of its OWN H(Y), never on
raw nats: each geometry has a different class balance, hence a different
finite-sample bias and a different amount of information there to find,
so raw MI would rank the most BALANCED label rather than the most
PREDICTABLE one. The break-even win rate m/(m+k) is printed beside each
so the ranking is read next to the bar the model must clear.

Stated in the output because it is the easy thing to get wrong: chance
precision EQUALS break-even at every geometry, so a tighter target does
not hand you expectancy. It buys predictability - less noise piled on top
of what the entry state knows - which is the one thing changing topology
cannot do.

Read-only by construction: it relabels a sampled copy via
TripleBarrierLabel(), never writes the label cache (which belongs to the
configured geometry), and restores the horizon and overrides it borrowed.
The overrides apply only when BOTH are positive, so a half-set pair can
never silently relabel a live run.

Compiles 0 errors / 0 warnings, standard and Market. Build tag
geometry-scan-v1. Redeploy only - no retrain to READ the ranking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:32:04 -04:00
AnimateDread
4443ce85c1 fix(diag): the alignment scan cried misalignment at its own arithmetic
First run came back "WARNING - peak at k=+5, NOT 0 ... a feature/label
misalignment upstream of every topology". That was a false alarm produced
by the diagnostic's own design, and exactly the kind of plausible-looking
output this project has lost days to.

Bar indices are MQL5 SERIES indices - HIGHER index = OLDER bar
(TripleBarrierLabel walks its window as `for(t = idx-1; t >= idx-horizon;
t--)`, decreasing index = forward in time). The two directions therefore
mean opposite things and the scan treated them as symmetric:

  k < 0  label belongs to a NEWER bar, its barrier window opens AFTER the
         features exist. Nothing at bar i can legitimately know it, so a
         peak here is real lookahead and a bug.
  k > 0  label belongs to an OLDER bar, already k bars into its window by
         the time bar i happens - so the features hold the realised first
         k bars of that outcome. MI MUST rise with k. Arithmetic.

Only the k<0 side can indict the pipeline, and on the observed data it is
clean: -5/-3/-2/-1 all sit at or below the k=0 value and the noise floor,
so there is no lookahead - a real negative result, not an absence of
evidence.

The k>0 side is now reported as what it is, a second positive control,
with its gradient as the finding: 0.01881 at k=+5 against 0.00401 at k=0
means ~4.7x more is knowable 5 bars into a 128-bar window than at the
entry the model actually trades on.

Compiles 0 errors / 0 warnings. Build tag mi-align-v2. Redeploy only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:25:26 -04:00
AnimateDread
87c8656b53 diag(autotune): a positive control, and a scan that separates "no signal"
from "signal knocked out of step"

Four architecturally different networks landed on the same precision -
Buy 23-25% against a 25.4% base rate, Sell 19-22% against 22.0% - while
making completely different calls (HYBRID votes Sell on 69% of bars, PAI
on 41%). Precision equal to the base rate is what INDEPENDENCE looks
like, and precision under independence is fixed by the label
distribution, not by the architecture, so all four converging on it is
arithmetic rather than coincidence. Accuracy meanwhile tracks coverage
exactly as independence predicts (31.1/30.3/25.0 predicted vs
31.8/28.9/24.6 observed for PAI/CONV/HYB).

But "no information in the data" and "information destroyed upstream of
every topology" produce that identical picture, and the MI test alone
cannot tell them apart either. Two additions:

POSITIVE CONTROL. Three "measurements" in this codebase have turned out
to be silent no-ops that produced plausible numbers - the MI scorer
reading an array nobody filled, the eval-mode guard that switched off the
imbalance correction, the alternation gate whose premise was never true.
So the estimator now has to prove it responds to a signal known to be
present before any floor reading is believed: the label of a neighbouring
sample row, ~19 bars away and far inside the 128-bar barrier horizon, so
the two outcome windows overlap heavily and MUST be associated. Same
binning, same estimator. Near the floor => every MI figure is void.

ALIGNMENT SCAN. Re-scores against the label taken from bar i+k for k in
-5..+5. A peak at k != 0 is a feature/label misalignment - an off-by-one
in the label index, a horizon applied to the wrong bar, a feature window
that lags what it claims - which would destroy the information before any
topology saw it and would look identical in every accuracy number this EA
prints. A flat profile says the features simply do not carry this target.
The sampled range is trimmed by |k| at both ends so a shift is measured
rather than an edge effect, and both bars must carry a real label.

Also: BuildMiSample publishes its stride instead of the report
recomputing that arithmetic (it would drift), and the control sizes its
buffers from its own sample count rather than the caller's.

Compiles 0 errors / 0 warnings, standard and Market.
Build tag mi-control-align-v1. Redeploy only - no retrain, no model
deletion; the diagnostic runs on resumed models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:12:10 -04:00
AnimateDread
9a5f645dc3 diag(autotune): stop making the feature test cost a trained model
The permutation test lived inside TuneIndicatorsByFilter, which is gated
on era 0 - correctly, because re-running the SWEEP would change the input
vector out from under weights already fitted to the old one. But the test
itself reads cached features and writes nothing, so none of that applies
to it, and the gate meant the only way to see the answer on a running
model was to delete the model. Today that price was PAI's 45 trained eras
and CONV's 31, spent to re-ask a read-only question.

Split into ReportFeatureLabelInformation(), called from the sweep when it
runs and directly when it does not - a resumed model, a disabled tuner,
nothing tunable. Once per attach either way.

Compiles 0 errors / 0 warnings. Build tag permtest-v2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:01:32 -04:00
AnimateDread
9920754dec diag(autotune): five permutations was still a coin flip - use a real test
The 5-draw z-score shipped an hour ago disproved itself on its first run.
All four charts scored the IDENTICAL 0.00401 nats on identical features
and identical labels - and reported z of +1.3, +2.0, +4.0 and +4.7. Two
"AT THE NOISE FLOOR", two "a real association", same data. The entire
swing came from estimating the null's spread from five draws, where the
standard deviation of the standard-deviation estimate is ~35%: the
denominator was noisier than the effect it was judging.

Replaced with an empirical permutation test. 200 draws, p counted by rank
with the +1/(B+1) correction (Phipson & Smyth 2010) so p is never
reported as exactly zero - no normality assumption and no spread to
estimate. The strongest single column is tested against the null
distribution OF THE MAXIMUM, which corrects for scoring 26 features at
once by construction and is far less conservative than Bonferroni.

Affordable because BuildMiSample is now split out of ScoreCurrentParamsByMI
and runs ONCE for the whole test - every draw reuses that sample and costs
a relabel plus 26 histogram passes, not 2000 feature extractions. The
coordinate sweep still calls the combined form, which is correct there:
each candidate changes the indicator settings, so its features really do
have to be re-extracted.

The verdict line keeps both questions apart and prints both answers: the
p-value for "is it real", the excess as a percentage of H(Y) for "is it
big enough to trade". At n=2000 those can disagree, and collapsing them
into one word is how a worthless effect gets called a discovery.

Compiles 0 errors / 0 warnings. Build tag permtest-v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:45:46 -04:00
AnimateDread
12a1fbd133 diag(autotune): one label shuffle cannot settle the no-edge question
The permutation baseline added in 018afb1 came back on all four charts as
0.00401 nats against floors of 0.00267 / 0.00298 / 0.00318 - three draws
whose spread is as wide as the excess being judged, because one shuffle
is one sample from the null, not the null. That is not enough to retire a
topology on.

Now MI_NOISE_PERMUTATIONS draws, reported as mean +/- sd with a z-score,
plus two numbers the mean over 26 columns cannot express:

  - the STRONGEST single feature's MI, against its own shuffled value.
    One informative column among 25 useless ones is precisely the case
    the mean hides, and precisely the case worth finding.
  - the excess as a percentage of H(Y). At these sample sizes a z-score
    can be comfortably significant while the effect is worthless, so
    "is it real" and "is it big enough to matter" are asked separately
    and answered separately.

The verdict line also now states the measure's limit every time rather
than only when the news is bad: this is a MARGINAL, PER-BAR statistic and
the network reads m_historyBars bars jointly, so it can prove signal
exists but never that it does not. It rules out a per-feature edge - and
therefore any indicator retuning - not an edge that lives in a
combination or across time.

Compiles 0 errors / 0 warnings, standard and Market.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:32:12 -04:00
AnimateDread
89eab14ca8 fix(chart): arrows survived the EA that drew them - persist, then clear
Reported: on deinit the panel and status label go, the signal arrows stay.

Two independent causes, both fixed here.

1. It was partly deliberate. ShutdownChartCleanup carried a second
   behaviour selected by a `preserveChartArrows` flag derived from the
   deinit reason: on RECOMPILE / PARAMETERS / CHARTCHANGE / TEMPLATE the
   arrows were left on the chart on purpose, to avoid a reload flicker.
   That branch IS the reported symptom, an operator cannot tell it apart
   from a cleanup that failed, and it was outright wrong whenever the
   reload changed the config - REASON_PARAMETERS means exactly that, and
   the preserved arrows then belonged to a model the chart no longer
   runs, with nothing marking them stale. It is gone, along with the flag
   and m_purgeChartOnDestruct. One path now: persist, clear, restore on
   the next attach.

2. Whatever remains was unfalsifiable. PurgeChart was a single
   ObjectsDeleteAll(prefix) whose return value was discarded, with no
   caller ever looking at the chart again - so "the arrows are still
   there" and "the arrows were never there" produced identical evidence,
   which is why the report survived three sessions. It now verifies:
   after the bulk delete it walks the OBJ_ARROW-typed list (a handful of
   objects, not the whole chart), deletes any surviving WarSig_ by name,
   and says so. Costs one typed scan when the bulk delete works, which is
   the normal case; names the root cause when it does not.

Every failure mode of SaveChartSignals was also silent - it returned void
and had three bare early returns. It returns bool now, logs the open
error with the filename, and the shutdown purge is CONDITIONAL on it: for
a converged model the chart objects are the only copy of its signal
history (nothing redraws them - the renderer runs per training era and a
deployed model has none left), so a chart left littered because the disk
write failed beats a clean chart bought by destroying the history. Either
way the log now says which happened.

Also states the user's rule once, where arrows come back rather than
across InitNeuralNetwork's several exits: no weights loaded for this
config => clear the sidecar and start visually clean. A fresh run must
not inherit calls it never made, and the first save would otherwise adopt
them (the sidecar is rebuilt by scanning the chart).

Compiles 0 errors / 0 warnings, standard and Market. Needs redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:28:34 -04:00
AnimateDread
018afb1ba9 fix(autotune): MI scorer read an array nobody filled; add the permutation floor
THE TUNER WAS A SILENT NO-OP. Every chart logged

  auto-tune complete - 17 candidate settings scored in ~139s,
  feature/label mutual information 0.0000 -> 0.0000 nats (no improvement)

0.0000 is not a weak result, it is a broken measurement: finite-sample MI
is biased UPWARD, so even pure noise scores above zero. Cause:
ScoreCurrentParamsByMI called BufferTempDataCompute(), which APPENDS the
bar's features to TempData and never touches m_featureCache - only the
caching wrapper BufferTempData() writes that array. It then read
m_featureCache, which ReInitADIndicators had just invalidated. Every
column came back constant, FeatureColumnMI returned 0 for all of them,
and all 17 candidates tied at exactly zero. 139 s per chart to return the
settings it started with.

Now reads the values back out of TempData, where they actually land. And
an exactly-zero best score is called out as a fault rather than reported
as "no improvement", because that is what it is.

ADDED: a PERMUTATION BASELINE, which is the diagnostic this project has
been missing. MI's finite-sample bias is ~(bins-1)(classes-1)/(2n) nats -
at these sample sizes the same order as any real edge in this domain - so
a raw MI figure is uninterpretable on its own. Shuffling the labels
destroys every genuine association while leaving sample size, binning and
class proportions intact, so the score it produces IS this dataset's
noise floor, measured rather than approximated. The log now reads

  feature/label information - X nats against a shuffled-label floor of Y

and says outright whether the features carry usable information about the
target. It needs no training, no topology and no convergence, so unlike
every accuracy number in this codebase it cannot be confounded by an
optimizer or an objective. If the score sits on the floor, no change of
architecture can help - which is the question the last three days of
zero-edge results have been circling.

DEPLOY FLOOR: `dirPrecPct > chancePrecPct` passed anything above chance by
any amount. At ~11,000 directional calls the standard error of the
precision estimate is ~0.4pp, so that gate was accepting sub-one-sigma
noise - the perceptron deployed at edge +0pp on 2026-08-01. Now requires
EDGE_MIN_SIGMAS (2.0) standard errors above chance, computed from the
actual call count, so the bar scales with the evidence instead of needing
a hand-picked constant.

Recorded with it, because it is why chance is the right reference at all:
under a driftless random walk P(touch +k*ATR before -m*ATR) = m/(m+k),
and the break-even win rate for a k:m reward:risk trade is ALSO m/(m+k).
The label's own base rate IS the break-even rate, at every SL/TP setting.
So "beats chance" and "is profitable" are the same test, and no choice of
SL/TP can manufacture an edge - only prediction can.

Both builds compile 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:05:50 -04:00
AnimateDread
d7eea325fb refactor(ai): extract Layer.mqh and deduplicate AI config
- Moves CLayer neuron construction to AI/Impl/Layer.mqh to keep Network.mqh clean
- Unifies four previously duplicated architecture initialisation blocks (MLP/CONV/LSTM/HYBRID) into a single shared function
- Eliminates risk of behavioural drift where one architecture missed a setter, causing mismatched feature sets or targets
2026-08-01 11:27:28 -04:00
AnimateDread
36a2825087 chore(Network): remove unused optimization methods and tidy whitespace
Remove the unused SetOptimization/Optimization virtual getter/setter from
CNeuronBase and the static member `alpha` initialization. These were dead
code. Also fix trailing whitespace inconsistencies in comment blocks.
2026-08-01 11:26:59 -04:00
AnimateDread
9756e2b64f fix(deinit): O(n^2) arrow prune blew the shutdown budget and littered 3 charts
Reported as "the perceptron correctly cleaned its chart on deinit, the
other 3 did not, abnormal termination". Measured from the 2026-08-01 log,
time from "OnDeinit: shutting down" to MetaTrader force-terminating:

  PAI     3.75 s  -> survived, chart cleaned
  CONV    4.71 s  -> Abnormal termination
  LSTM    4.28 s  -> Abnormal termination
  HYBRID  4.16 s  -> Abnormal termination

In all four the last line printed is the inference census, which is the
end of StopTraining() - so the overrun is inside ShutdownChartCleanup(),
i.e. between saving the arrows and purging them.

The cost is the prune loop at the end of SaveChartSignals():

    for(int i = 0; i < prunedCount; i++)
       ObjectDelete(0, SIG_ARROW_PREFIX + TimeToString(pruned[i]));

ObjectDelete is O(objects) on a crowded chart, so this is O(n^2). It was
harmless while the model called a direction on ~6% of bars. After the
triple-barrier relabel the models call on 83-94% of bars, the chart
carries many thousands of arrows, and the loop overran MetaTrader's
OnDeinit budget - so PurgeChart() never ran and the arrows stayed on
screen. The slow tidy-up starved the fast one.

The work was pure waste at that moment: ShutdownChartCleanup purges every
arrow with a single bulk ObjectsDeleteAll immediately afterwards.
Deleting them one at a time first has no effect except to prevent the
bulk delete from happening at all.

SaveChartSignals takes a pruneChartObjects flag, and the two shutdown
call sites pass false:

 - ShutdownChartCleanup passes `preserveChartArrows`, which is exactly
   right: prune when the arrows are STAYING (chart and sidecar must
   agree), skip when they are about to be purged wholesale.
 - FinalizeTrainRun passes !m_trainingStopRequested. Removing a chart
   MID-ERA reaches StopTraining -> FinalizeTrainRun, which took the
   expensive path a second time, even earlier, before anything had been
   cleared. Same defect one call site up; it only escaped notice because
   the observed removals happened to land between eras.

Normal convergence and the live per-era path are unchanged - they still
prune, which is what keeps the chart object count bounded.

This also restores the invariant the 2026-07 fix intended ("chart cleanup
runs BEFORE the heavy weight save so a stall cannot leave the chart
littered"). That fix moved cleanup ahead of the WEIGHT save, but cleanup
had since grown its own slow step ahead of its own fast one.

Both builds compile 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:38:36 -04:00
AnimateDread
6db0519472 perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
  rung 0: 8 cand x 3 seeds x  3 eras =  72 eras
  rung 1: 4 cand x 3 seeds x  8 eras =  96
  rung 2: 2 cand x 3 seeds x 20 eras = 120
  = 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:

  PAI     29.1 s/era  ->   9.3 h   (matches the observed 00:37 -> 09:22)
  CONV    41.3 s/era  ->  13.2 h
  LSTM   150.4 s/era  ->  48.1 h
  HYBRID 154.6 s/era  ->  49.5 h

Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.

It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.

THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.

So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.

Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.

Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.

HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.

Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.

AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.

The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.

Both builds compile 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
AnimateDread
eafc6802d9 fix(ui): panel claimed "no directional calls" while the model was signalling
Reported as "they seem to be signaling but the label stays stuck at no
directional call yet". The model was right and the panel was wrong.

m_cumIsTotal/m_cumOosTotal are LIFETIME, persisted counters - they are
what the panel presents as the product's accuracy - so they deliberately
skip m_evalMode bars: a throwaway auto-tune candidate must not pollute
the deployed model's reported win rate. That gating is correct and
stays.

The consequence was not handled. While an auto-tune search runs, EVERY
era is an eval-mode candidate, so both counters stay at zero for the
entire search while the model trains, signals, and draws arrows
normally. The panel therefore reported "no directional calls yet" -
directly contradicting the chart the user was looking at - for what is
the longest phase of a first run.

Three states now get three messages:
  - search running      -> "tuning (round N of M) - measured after"
  - final winner retrain-> "training final model..."
  - genuinely no calls  -> "no directional calls yet" (era > 0), or
                           "measuring..." before the first era

Round-level progress rather than a bare "tuning" because each candidate
is a full training run repeated across seeds and generations, so this
phase runs for hours; a progress-free wait is indistinguishable from a
hang, which is how it was read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:10:56 -04:00
AnimateDread
e83e30344f fix: disable auto-tune indicators by default
The AutoTuneIndicators input is now automatically derived via `ComputeTuneTrialBudget()`, so the default is set to false to prevent manual interference.
2026-08-01 10:03:23 -04:00
AnimateDread
3bae2f9254 fix: the imbalance correction never ran during the auto-tune search
Neutral collapse on all four topologies by era 5 with a 2:6 barrier
(recall Buy 0% / Sell 0% / Neutral 100%), and the panel stuck on
"measuring...". One root cause, and it was not the barrier.

The labels were fine: Buy 25.4% / Sell 22.0% / Neutral 52.5%, which is
exactly gambler's ruin for m=2,k=6 (2/8 = 25% per side), with only 0.1%
of Neutral coming from the vertical barrier - so the new m*k horizon
scaling is right, arguably generous.

What was broken: Train()'s era-start block wrapped UpdateClassPriors() in
`if(!m_evalMode)`. The auto-tune GA scores every candidate in eval mode,
and AutoTuneIndicators ships ON, so on a default configuration EVERY era
of the search ran with unmeasured priors. ApplyLogitAdjustment() requires
measured priors; without them it calls ClearLogitAdjustment() and returns.

So the entire search trained under PLAIN cross-entropy. With a 52.5%
majority class the optimum of plain CE is "always predict Neutral", and
that is precisely what all four models found. The panel followed: its
counters only advance on bars the model CALLED Buy or Sell, so a
collapsed model leaves them at zero and the line reads "measuring..."
forever.

This was latent, not new. It has been true for every auto-tuned run, but
it was invisible while the labels were near-balanced - last night's
accidental 1:1 barrier gave 43/40/17, where plain CE has no majority to
collapse into. Widening the stop to 2*ATR (correctly - 1*ATR is too tight
to survive noise) moved Neutral to the majority and exposed it.

The guard's stated fear cannot happen. These priors are measured from the
LABEL distribution, and the tuner only perturbs indicator periods
(MA/RSI/MACD/Ichimoku/AD). The barrier label depends on ATR, SL_Mode and
TP_Mode - none of which the search touches - so every candidate sees
byte-identical labels and identical priors. There is nothing to
contaminate. What the guard actually protected was the .stats write, and
that is gated separately: eval candidates never checkpoint and never
persist.

Also, because this is the THIRD quiet no-op to cost a run in this
codebase (after the fictional oversampling log line and the shadow-blend
skip):

- ApplyLogitAdjustment() now WARNS when it declines to install, instead
  of silently clearing. A mechanism that cannot announce it is not
  running is indistinguishable from one that is.
- The panel distinguishes "measuring..." (before era 1, nothing scored
  yet - an honest warm-up) from "no directional calls yet" (eras trained,
  zero calls - a finding, not a wait).

Both builds compile 0 errors / 0 warnings. No retrain forced by this
commit itself, but the collapsed models must be discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:46:24 -04:00
AnimateDread
8ff3f5d632 refactor(inputs): set default SL to ATRx2 and TP to ATRx6
Adjust default stop-loss mode from SL_ATR_x1 to SL_ATR_x2 and default take-profit mode from TP_ATR_x3 to TP_ATR_x6. This improves the risk-reward alignment in line with the recommended minimum ratio and ensures setups are not rejected under the EA's target reward parameters.
2026-08-01 00:34:58 -04:00
AnimateDread
25813523d3 fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.

1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.

   `OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`

   -101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
   7eb48f5. MetaTrader does not validate a saved enum input against the
   enum's current members, so charts saved before that kept the old
   integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
   then quietly turned it into a 1:1 barrier, and all four topologies
   trained ~250 eras against a strategy nobody selected - while the log
   reported "target 1.00*ATR" as though it were configured.

   Since the relabel these two inputs ARE the label definition, so this
   is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
   Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
   on any value that is not an enum member. Members are enumerated rather
   than range-checked because both enums are sparse and carry negative
   sentinels, so no min/max test can tell a legal value from a deleted
   one - which is the entire failure mode. The fallback survives as
   belt-and-braces but now announces itself: a fallback that cannot say
   it fired is indistinguishable from correct behaviour.

2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.

   `tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
   is Buy+Sell as a share of all bars. At the old exact-pivot target that
   was ~6%, so "beat the base rate" read as "beat chance" and the test
   looked sound. Triple-barrier labels put it at ~83%, so the gate now
   demanded 83% directional precision - impossible by construction.
   Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
   safe to deploy" at a perfectly healthy 43-45% precision, with no
   checkpoint able to ship however good it got.

   Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
   score of the degenerate always-call-one-direction model this floor
   exists to reject. Correct at any base rate - ~43% on the current
   labels, ~3% on the old rare-pivot ones. The era line now prints
   "(chance N%, edge +Mpp)" beside the selection score, because 44%
   precision is excellent against a 3% chance level and worthless against
   a 43% one, and reading the first as the second is what made tonight's
   run look better than it was.

3. THE HORIZON IGNORED THE BARRIER GEOMETRY.

   ComputeBarrierHorizonBars() returned the median ZigZag leg, which
   measures how long a ~1 ATR move takes and says nothing about how long
   the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
   with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
   horizon would have timed out most 1:3 trades and pushed Neutral
   straight back up, re-creating the imbalance the relabel removes.
   Now multiplied by slMult*tpMult, calibrated against a real measurement
   rather than assumed: the accidental 1:1 run resolved at horizon 12 with
   only 16.7% timeouts, so the swing median is the right scale at m*k=1.

   Verifiable, not just asserted: the prebuild now counts barriers that
   ended on the VERTICAL barrier and reports them as a share of Neutral.
   Neutral conflates "timed out" with "stopped out" and only the first
   indicts the horizon.

Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
AnimateDread
f48bc93f9b refactor(inputs): 96 -> 70 inputs; remove two untested/unusable filter modules
Every removal below is FINGERPRINT-NEUTRAL by construction: each retired
input is pinned to the exact value it already shipped with, so running
models keep their filenames and resume rather than restarting at era 0.
Verified field by field against BuildConfigFingerprint.

Removed as inputs, kept as pinned constants (the value was never a
preference the user had a basis to change):

- OutputNeuronsCount. The regression head predicts a continuous quantity
  the triple-barrier label does not contain; the target is an EVENT, so
  the right output is its probability. The regression code paths stay
  implemented and dormant - they cost nothing and removing them would
  touch every scoring path at once.
- MinRecall. A safety floor, not a preference, and the only direction a
  user can move it is the harmful one: raising it past what the config
  reaches yields NO model, not a better one (observed repeatedly at 60).
- SwingConfirmationBars. Stopped gating the labels with the relabel, but
  is STILL load-bearing for the swing-context input features - it is the
  ZigZag repainting embargo, and without it those 9 features read a leg
  the live bar could not have had yet. Pinned, not deleted.
- MaxErasPerRun (runaway backstop, never reached in a healthy run),
  FreezePriorCalibration (unanswerable by a user; near-balanced labels
  make the priors stable anyway), VerboseMode (developer view, joins
  DebuggingMode), MACD/Ichimoku periods x6 (both indicators ship
  disabled, and as optimizer dimensions they are pure overfitting
  surface - the AI auto-tuner is the supported way to move them).
- SignalClusterWindow -> 3, no longer an input. Barrier labels make
  consecutive setups real, which argued for 0; it is not 0 because on D1+
  a 6-bar window spans over a week and two arrows a day apart on a
  weekly-scale move are one event. 3 splits it correctly by timeframe.
- EnableOnlineLearning -> ON. Adapting to a changing market is what keeps
  a months-attached model from going stale, and the rolling-accuracy
  freeze is what makes it safe. See the caveat noted in the handoff: it
  had not been forward-tested on a live feed when this became default.

Removed entirely:

- Intraday Time Filter (5 inputs + Signals/SignalITF.mqh). Two of its
  five inputs were raw BITMASKS, which is an implementation detail
  exposed as a control. The job is covered three times over by things
  that are declarative or that learn: the session filter, the
  time-of-day/day-of-week input features (the network discovers which
  hours are good rather than being told), and the journal's time buckets.
- Market Depth Filter (5 inputs + Signals/SignalMarketDepth.mqh, plus
  its OnInit probe and OnDeinit release). It needs real level-2 data
  that this broker - and most retail MT5 brokers - do not provide, so
  the module has never once executed against real data. Shipping four
  tuning dropdowns for an untested path is worse than shipping nothing:
  the only users who could enable it would be its first-ever testers,
  live. If DOM returns it should be a FEATURE fed to the network, not a
  rule-based veto with hand-tuned thresholds - imbalance is data.
- IndicatorTuneTrials, replaced by ComputeTuneTrialBudget(). The useful
  budget depends on how many parameters are actually being searched,
  which depends on which features are enabled - so one number meant
  wildly different things run to run. The shipped 32 was ~10 candidates
  per dimension against one enabled indicator (wasteful: each costs
  GA_SEEDS full training runs) and under one per dimension against all
  nine (blind). Now population ~ 4 x active dimensions, clamped [8,64],
  with CADIndicatorTuner::ActiveDimensions() defined immediately above
  PerturbRandom() so the two cannot drift apart.
- Six orphaned enums (TUNE_TRIALS_PRESET, DOM_*, ENTRY_HOUR_OF_DAY,
  TIME_FILTER_DAY_OF_WEEK), 81 lines.

Other UX:

- SL_ATR_x1 / TP_ATR_x3 now carry the "(classic)" default marker every
  other preset enum in the file already used. Nothing in the SL/TP
  dropdowns previously told a user which pair was the shipped default -
  which matters far more since the relabel, because those two define the
  labels and changing either forces a retrain.
- Neural Network section moved directly ABOVE AI Input Features: choose
  the architecture, then choose what it sees. NN Optimizer / Performance
  stays last - the Adam/Sgd inputs are declared in AI/Network.mqh and
  render immediately after that divider.
- News feature + window moved to the end of the AI feature list, below
  Wyckoff Bar Inversion.
- Dropped "(0-100)" from Min vote to open - it is an enum, not a number.

Both builds compile 0 errors / 0 warnings. No retrain forced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:22:02 -04:00
AnimateDread
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>
2026-07-31 20:39:49 -04:00
AnimateDread
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>
2026-07-31 18:24:32 -04:00
AnimateDread
0261c013d0 fix: EMA shadow never received the LSTM weight block
Confirmed live the first run after b4d640d made the warning specific:
"1 weight block(s) could not be blended into the EMA shadow (layer 2,
neuron type 30852)" - 30852 is defNeuronLSTMOCL - on BOTH the LSTM and
HYBRID charts. Exactly what the .nnw sizes predicted (HYBRID's shadow was
791,120 bytes short of its live net, 4 x 24,704 doubles = the LSTM block
plus its Adam moments).

Mechanism: EnsureShadowNet() clones via Net.Save() -> clone.Load(), and it
is reachable from RefreshLatestSignal(), which runs before the live net's
first forward pass. CNeuronLSTMOCL::Save writes m_iInputs = -1 and omits
every LSTM buffer in that state, so the clone came back with WeightsLSTM
== NULL. Only the LIVE net ever runs forward, so the lazy SetInputs() that
would have allocated it never fired on the shadow - permanently. The blend
skipped the layer every era and returned true.

This is a live-inference and deployment defect, not a training one: the
shadow is the net RefreshLatestSignal and the deploy path read.

Fixed by self-healing in the blend rather than by reordering the bootstrap,
so an already-bad shadow on disk repairs itself too. New
CNeuronLSTMOCL::AdoptShapeFrom copies m_iStepInputs first (SetInputs reads
it to choose between the sequence and single-timestep block shapes) then
sizes the block; the blend then copies the live weights outright rather
than blending tau of them into fresh random ones - an EMA seeds at its
first observation.

Both builds 0/0. Needs redeploy; no retrain (training reads the live net).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:52:41 -04:00
AnimateDread
ab84998d35 feat(ai): true multi-bar conv and true sequence LSTM
CONV and LSTM were each configured as a strictly lossier perceptron, which
is exactly what the panel showed: PAI 24% > CONV 18% > HYBRID 12% ~ LSTM
12%, monotone in how much reaches the dense stack (420 / 160 / 32 / 16).

CONV - receptive field 1 -> 3 bars, and the pool is gone.
  Reading the reference kernels settled why 34d6aa4 killed CONV.
  FeedForwardConv emits POSITION-MAJOR output (matrix_o[out + window_out*i]),
  and FeedForwardProof is a flat contiguous max over `window` at stride
  `step`. On that layout any window <= window_out maxes ACROSS FILTERS
  within one position - it cannot pool over time at all. Our stage used
  window = step = filterCount: one max over all 8 filters per position,
  discarding 87.5% of the conv output and leaving only the argmax filter
  with gradient. That is a property of the reference's layout, not a
  porting bug, so there is no correct pool to swap in. Springenberg et al.
  ICLR 2015 is the answer already cited in this file: no pooling, get the
  hierarchy from strided convolution. The second conv went with it - its
  window was counted in raw elements while its comment claimed positions,
  so a "2-position" window actually spanned 2 filters of position 0.
  Filter count now derives from the WINDOW (RF * features / 2) instead of
  one bar, which at RF 3 was under-sizing the stage 3x.
  Shape: 20 bars x 21 -> 18 positions x 16 filters = 288.

LSTM - sequence mode back on, forget bias 2.0 -> 1.0.
  The forward path rules out the "no gradient" reading of the 2026-07-30
  failure: CPU_LSTMSeqForward starts every sample at h_0 = c_0 = 0 and
  unrolls that sample's own window, so nothing leaks between shuffled
  samples. Flat IS error + Neutral:100% is equally the signature of an
  output that does not vary with the input, and that is what bias 2.0
  produces: c* = i*g/(1-sigmoid(b)) ~ 8.3*i*g, |c*| ~ 4.2, tanh pinned at
  0.9995 with derivative 1e-3, so h_T is near-binary and set by the gate
  biases rather than the bars. Choosing 2.0 off the reach sweep was a
  method error - reach trades against saturation and the sweep never
  measured saturation. 1.0 is the Gers/Jozefowicz/Keras default and leaves
  tanh derivative ~0.1.

Both builds 0/0. DLL unchanged (CPU_LSTMSeqForward/Backward already
exported). Forces a retrain of CONV, LSTM and HYBRID - the .nnw pins
architecture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:42:33 -04:00
AnimateDread
b4d640d0b4 fix: blend-skip warning fired on layers that correctly own no weights
32880f3 warned whenever either side of the EMA blend returned no weight
block. Both-empty is normal: a dense layer whose successor owns the weight
matrix has none by design (CNeuronBaseOCL::Init only allocates Weights when
numOutputs > 0), which is the same reason LayerLearningReport prints
NOWEIGHTS for every dense layer in these topologies.

Result on the first run with it: "5 weight block(s) could not be blended"
on CONV and 6 on LSTM/HYBRID, type 30851 = defNeuronBaseOCL - all of them
the weightless dense layers. Pure noise, and it pointed at the wrong thing.

Only an ASYMMETRY is a defect: live has a block, shadow does not. That is
the HYBRID case the file sizes showed (shadow 791,120 bytes short, exactly
the LSTM weight block plus Adam moments), and it still trips the warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:11:16 -04:00
AnimateDread
32880f3728 diag: make BlendWeightsFrom say when it skips a layer
Every branch of the EMA shadow blend is `if(both sides return weights)
{ blend }` with no else. That silent degrade is deliberate - a partial
topology mismatch should not corrupt unrelated layers - but it also hid a
real defect for 343 eras on SP500 H1.

The HYBRID shadow's LSTM layer had never run a forward pass, so its
WeightsLSTM was still NULL and getWeightsLSTM() returned 0. The blend
skipped a 24,704-weight layer on every single era and returned true. The
only trace was on disk: the shadow .nnw is 791,120 bytes smaller than its
live net - 4 x 24,704 doubles, exactly the LSTM weight block plus its Adam
moments - while the CONV, LSTM and PAI shadows byte-match their live nets.

This matters beyond training: the shadow is the net live inference and
deployment read, so those layers are not tracking the trained model at all.

Counts skipped blocks and warns once per net, naming the layer index and
neuron type. Reporting only - the blend behaviour is unchanged, and the
underlying cause still needs a fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:38:09 -04:00
AnimateDread
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 397b0ea:
the plateau ladder's terminal stage still claimed "after a warm restart
AND full gamma anneal", and the CONVERGED line "across a warm restart and
a full focal-gamma anneal". Both printed on every CONV/LSTM convergence
today. Same failure mode as the oversampling line 397b0ea fixed: a log
that describes what an older version would have done is confirming
evidence for a false hypothesis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:33:29 -04:00
AnimateDread
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>
2026-07-31 11:46:57 -04:00
AnimateDread
7eb48f5038 feat(trade): anchor SL and TP to the entry price, not the last swing
Stops keyed to the recent swing extreme make a trade's risk a function of
how far the last swing happens to sit rather than of current volatility. On
a shallow pullback the swing sits close to the fill, so the stop is tight
enough to be taken out by noise on setups that then run to target - which is
what the Perceptron's signals were showing.

  SL: lowest_low/highest_high -/+ mult*ATR   ->   entry -/+ mult*ATR
  TP: TP_PREV_SWING (opposite swing)         ->   removed; ATR-from-entry
  SL_PREV_SWING, TP_PREV_SWING               ->   removed from the enums

The SL anchors to `price` (the resolved entry), not to base_price: with a
pending entry those differ by the whole entry offset, and the risk Money
sizes against is entry-to-stop.

MIN_SL_ATR_MULTIPLIER 2.0 -> 0.5. That floor existed because a swing-
anchored stop could land arbitrarily close to the entry and needed a bound
unrelated to the chosen multiple. An entry-anchored stop is exactly
mult*ATR by construction and cannot collapse, so leaving it at 2.0 would
have silently overridden SL_ATR_x1 to 2*ATR - making the input a lie AND
forcing TP >= 4*ATR just to clear the default 1:2 rejection filter. The
broker's own stop level is enforced separately and precisely by
TCAdjustStops(), so this is now a pure sanity net.

Default TP_Mode TP_PREV_SWING -> TP_ATR_x3, so SL_ATR_x1 + TP_ATR_x3 gives a
realised 3:1 against the 1:2 filter. TP_ATR_x2 would sit EXACTLY on the 2.0
boundary where price-normalization rounding alone can reject the setup; the
default leaves a deliberate gap. This is the same interaction that once
rejected 100% of setups on every symbol (see TP_INTELLIGENT_BASE_RR).

Swing validity guards now reject only when the configuration actually uses a
swing - i.e. ENTRY_PREV_SWING. Previously an unsynced or thin history
rejected EVERY trade, including configurations whose levels no longer
reference a swing at all. The guards are kept, not deleted: a bad swing must
still never reach an entry price, and iLow/iHigh are no longer called with a
possibly-negative index.

TP_INTELLIGENT stays risk-relative. Now that risk is exactly mult*ATR the
risk- and ATR-relative forms coincide, but risk-relative keeps its
reward:risk guarantee exact after the floor or TCAdjustStops widens a stop.

Both builds compile 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 10:11:13 -04:00
AnimateDread
5a12ae08e2 revert(ai): restore the 4eae763 front-ends - both my rewrites stopped signaling
CONV and LSTM were signaling at 4eae763. Two changes I made after it each
broke one of them, and neither was caught by the metric I was reading.

CONV, same data one hour apart on SP500 H1:
  19:44  window = 1 bar   era 5: dir-precision 18%, 45 live fires, best bal 3.1 -> 4.5
  20:46  window = 2 bars  era 5: no directional calls, 0 live fires, best bal frozen at era 1

LSTM: the sequence rewrite has been live since 18:48 (the `lstm 420->64`
config line only derives that way under the new per-timestep budget) and has
been OOS recall Neutral:100% with a flat IS error in every era since, past
era 100.

Both are reverted behind a switch rather than deleted, because both
DIAGNOSES stand: a conv with a one-bar window is a 1x1 conv that cannot mix
across time, and the old LSTM really did apply one gate step to the whole
flattened input. What does not stand is shipping either on the strength of
an offline correctness proof.

  CONV_RECEPTIVE_FIELD_BARS 2 -> 1   (also drops pool + conv2 via
                                      HasSecondConvStage, restoring the
                                      exact 4eae763 front-end)
  LSTM_SEQUENCE_MODE            0     (single-timestep layer; sizing follows,
                                      since a recurrence budgets on the
                                      per-step width and this one does not)

Kept, because they are correct independent of the above:
  - per-layer dW/W era line (18f63c7) - the instrument that should have
    caught both of these in one era instead of a retrain cycle each
  - CNet conv/pool sizing-cursor fix (a pool stacked on a conv would have
    sized against a width window_out times too small)
  - .nnw architecture guard - forces the retrain this revert needs, since
    models trained since 20:46 carry a 2-bar conv tensor
  - LSTM_FORGET_BIAS_INIT and both offline checks (gradcheck 2.3e-10,
    flowcheck) - dormant at LSTM_SEQUENCE_MODE 0, correct when re-enabled

The lesson is in the two #define comments: a gradient check proves the math,
not that the layer trains in situ. Re-land each behind the dW/W line.

Both builds compile 0 errors, 0 warnings. Forces a CONV/LSTM/HYBRID retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 10:04:23 -04:00
AnimateDread
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>
2026-07-31 07:10:09 -04:00
AnimateDread
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>
2026-07-30 20:05:37 -04:00
AnimateDread
5a7ed73df6 fix(ai): positive forget-gate bias, giving the LSTM its window back
The sequence rewrite made LSTM/HYBRID recurrences over 20 bars, but every
weight - including the gate biases - is initialized around zero. That puts
the forget gate at sigmoid(0) = 0.5, so the cell state is halved every step:
the first bar survives into the output scaled by ~0.5^20, and the gradient
reaches it scaled by the same factor.

The layer was therefore a one-bar model wearing a 20-bar interface. A one-bar
model has no signal on this task, so the head learned the base rate and
emitted Neutral everywhere - the flat 0.34 IS error across twelve eras and
OOS recall Neutral:100% seen on SP500 H1.

Measured at the shipped H1 shapes (H=64, stepInputs=21, T=20) - influence of
bar 0 on the output relative to bar 19:

  bias 0.0 -> 3.0e-05 forward, 3.3e-05 backward   (dead)
  bias 1.0 -> 1.2e-02 forward, 1.4e-02 backward
  bias 2.0 -> 2.5e-01 forward, 2.7e-01 backward   (a real 20-bar field)

This is the standard fix, not a tuned knob: Gers/Schmidhuber/Cummins (2000)
introduced the forget gate with a positive bias, and Jozefowicz/Zaremba/
Sutskever (ICML 2015) recommend a bias of 1 as a default (whence Keras'
unit_forget_bias). Both 1 and 2 are standard; the sweep picks 2 because at a
20-step window a bias of 1 still leaves the oldest bar at ~1% influence.

lstm_seq_flowcheck.cpp is added as a permanent regression check and asserts
the shipped constant keeps >=5% reach in both directions. It complements
lstm_seq_gradcheck.cpp: that one proves the BPTT is CORRECT, this one proves
it is USABLE. The gradient check passed at 2.3e-10 throughout - correct math
over a recurrence that carries nothing looks exactly like a bad architecture.

Both builds compile 0 errors, 0 warnings. Initialization only, so the .nnw
format is unchanged; LSTM and HYBRID must retrain to pick it up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:15:29 -04:00
AnimateDread
b941977612 2 new files 2026-07-30 18:46:03 -04:00
AnimateDread
7a753e54ba feat(ai): sequence-LSTM kernels for the OpenCL tier
Closes the gap left by 7a08197, which refused sequence mode under OpenCL. That
was defensible for a private build and not for a shipped one: the release path
includes an OpenCL laptop, and a customer with a GPU would have found LSTM and
HYBRID simply unavailable.

One launch PER TIMESTEP rather than a single kernel looping with barrier().
Every hidden unit's gates read all of h_{t-1}, OpenCL barriers only span a
work-group, and nothing here constrains how the runtime partitions the global
size - so an in-kernel loop would be correct only by luck of the partitioning.
Host-driven launches make each step an implicit global barrier: more enqueues,
correct on every device.

Backward reuses the buffers the single-timestep path leaves idle in sequence
mode - ConcatenatedGradient (4H) for gate gradients, HiddenCache (H) for dh,
Memory (2H) for dc - so BPTT costs no extra allocations. dW is zeroed once and
accumulated across steps, matching the fused DLL kernel.

Verification available on this machine has limits worth recording. The math is
the same as CPU_LSTMSeqForward/Backward, which is gradient-checked to 2.3e-10;
the kernels are syntax/type-checked offline (DirectML\opencl_seq_syntax_check.cpp,
compiled as C++ with OpenCL shims) because there is no OpenCL device or ICD
here. That check exists because a typo in Network.cl fails the WHOLE program
build, which would take the dense and conv kernels down with it - not just the
new ones. KernelCreate results are now checked and reported for these four for
the same reason; a build failure degrades to "LSTM/HYBRID unavailable on this
device" instead of an Execute error mid-training.

STILL NEEDS A RUN ON REAL OPENCL HARDWARE before release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:40:56 -04:00
AnimateDread
7a081979b2 feat(ai): make LSTM/HYBRID actual sequence models over bars
CNeuronLSTMOCL consumed the whole flattened input in ONE gate computation and
back-propagated a single timestep, which its own class comment stated. Combined
with a conv stage whose window=step=neuronsCount gives it a receptive field of
exactly one bar, no stage in HYBRID mixed information across time - the bars
reached the dense stack as an unordered flat vector, the same thing the plain
MLP sees. That predicted the measured ranking (MLP 31.5%, CONV 32.5%, LSTM
30.6%, HYBRID 14.4%): each extra bottleneck cost accuracy and bought nothing.

The layer now unrolls m_historyBars timesteps, sharing one gate block across
them and carrying h/c forward, with real BPTT carrying dh and dc backward.
Per-step width comes from CLayerDescription::window, which CNet passes to the
new SetStepWidth() - previously dead metadata.

Consequences worth naming:
- Weight count drops from 4H(H+420+1) to 4H(H+21+1). Weight sharing is the
  point of a recurrence, so ComputeLstmHiddenSize now budgets on the per-step
  width; H goes 16 -> 64 at H1 defaults, and the model is still smaller.
- h/c start at zero per sample. The old buffers persisted across forward
  passes, so under shuffled training each sample inherited an unrelated
  sample's state.
- .nnw LSTM records are versioned (LSTM_SEQ_SAVE_TAG). The old weight block is
  a different shape, so Load REFUSES pre-rewrite models rather than misreading
  one and throwing off every later layer's offset. LSTM and HYBRID must retrain.
- Sequence mode has no Network.cl kernel, so it refuses the OpenCL tier loudly
  instead of quietly running a different architecture there than on the DLL
  tier - the two would train different models from one .cfg.

Legacy single-timestep path kept intact for step width <= 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:22:24 -04:00
AnimateDread
3ea54b4f2b feat(dll): fused sequence-LSTM kernels with real backpropagation-through-time
The per-step entry points cannot express a sequence model. CPU_LSTMGates takes
the ENTIRE flattened input as one timestep, and CPU_LSTMGateGradient has no
parameter for dc arriving from the following step - so the recurrent gradient
path does not exist and cannot be assembled from these primitives at any call
pattern. The layer built on them is a gated dense layer that the class comment
already described honestly: "single-timestep-truncated BPTT".

Adds CPU_LSTMSeqForward / CPU_LSTMSeqBackward: the whole unrolled sequence in
one call each, weights shared across timesteps, dW accumulated over all of them
(the per-step CPU_LSTMWeightsGradient assigns rather than accumulates, so it
could not have been reused even with the dc term). Fused rather than dispatched
per step because the recurrence is sequential - T round trips would serialise T
lock/dispatch pairs for a few thousand FLOPs each.

h_{-1} and c_{-1} are zero per sample. The old layer carried its cell state
across forward passes, so under shuffled training every sample inherited the
state of an unrelated one.

DirectML gets the same math host-side (readback, compute in double, upload)
rather than HLSL: the recurrence needs a barrier per timestep, the GPU buffers
are float and BPTT accumulation is where that hurts most, and no D3D12 device
exists on this machine to test a shader against. Documented at the definition.

Verified with lstm_seq_gradcheck.cpp - central-difference check of dW and dX
against an asymmetric loss over the final hidden state. Max relative error
2.3e-10 on both, with a non-trivial gradient magnitude asserted so the check
cannot pass on an all-zero result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:13:50 -04:00
AnimateDread
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
a142749. Two different metrics in one sentence, so HYBRID logged "regressed
from best 14.4% to 34.0%" a hundred times - a regression to a higher number,
which is not a thing. The comparison itself was right (selectionScore, coverage
weighted, genuinely below best); only the print was wrong. 1039ad9 relabelled
these strings but missed that this site passes the wrong variable.

The startup config line had the same shape of gap: it printed the dense taper
and called itself self-verifying while the DERIVED conv and recurrent stages -
the ones that dominate CONV/LSTM/HYBRID - were invisible. It now shows the
width into and out of each front-end stage, and flags the case where the dense
stack is wider than the vector reaching it (a linear fan-out cannot recover
what the bottleneck discarded; it only adds parameters). Flagged, not silently
reshaped - that would re-key trained topologies mid-comparison.

UsesConvStage()/UsesLstmStage() replace HasConvBeforeLstm() as the primitive,
so each subclass declares its composition once and both the capacity budget and
the config line derive from it rather than restating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:20:30 -04:00
AnimateDread
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>
2026-07-30 13:39:08 -04:00
AnimateDread
efdd36d183 fix(ai): stop the shutdown save from resurrecting reset weights; size HYBRID's LSTM to its real fan-in
ResetWeights already deletes the whole model set - .nnw, .cfg, _ckpt.tmp,
.stats, _shadow.nnw - and clears both the .arrows sidecar and the drawn
chart objects. What undid it was PersistWeightsOnShutdown: detaching the EA
after a reset but before an era completed re-created a .nnw from the
freshly-built, never-run net, so the next attach loaded an era-0 stub
instead of starting clean. For LSTM/HYBRID that stub is worse than nothing -
a layer that has never run a forward pass has m_iInputs<=0, so Save omits
every LSTM buffer (see 413ff7e). Skip the save when no era completed and no
model was loaded; that is exactly the post-reset and first-attach state.
Also sweep _shadowclone.tmp, which the reset did not cover.

Separately, ComputeLstmHiddenSize budgeted every topology against the
flattened input (historyBars x neuronsCount). True for LSTM, wrong for
HYBRID, where AddConvStage runs first and the LSTM is fed the conv feature
map - historyBars x convFilterCount, 160 rather than 420 at H1 defaults.
The quadratic is dominated by the inputs term, so overstating the fan-in
2.6x cost a full ladder step (16 units where the budget affords 32). New
virtual HasConvBeforeLstm() feeds LstmFanIn(), so composition decides this
rather than an AIType check. desc.window is advisory only - CNet never
passes it to the layer - but is now truthful for the same reason.

Derived values stay out of the weights-filename fingerprint and are adopted
from the .cfg, so existing models keep their saved width; only fresh ones
pick up the corrected budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:06:09 -04:00
AnimateDread
413ff7e9a2 fix(ai): allocate LSTM cell state on the load path, unpinning HYBRID from Neutral
CNeuronLSTMOCL::Save early-returns when m_iInputs<=0, writing no LSTM
buffers at all - correct, since a layer that has never run a forward pass
has no weights to persist. But Load mirrored that early return BEFORE
allocating Memory (the c_prev cell state), and SetInputs - the lazy sizing
path that runs on the next feedForward - allocates the weight buffers but
never Memory. Both Init overloads allocate it unconditionally, so only the
save-then-load round trip could produce the gap.

Net effect: any net serialized before its first feedForward came back with
Memory==NULL. LSTMGates then failed on every call, short-circuiting the ||
before LSTMState could dereference the null buffer, so instead of crashing
the layer computed nothing forever. Observed on HYBRID after a
weights-reset-then-detach: all three class outputs pinned at exactly 1.000
(spread 0.0000), IS error stuck at 0.78, Buy/Sell recall 0%, and 636k
"Error of execution DirectML LSTM feedForward" lines in one 55MB journal.
The model looked like a converged Neutral collapse; it was a dead layer.

Allocate Memory in Load ahead of the early return, and harden SetInputs to
guarantee every buffer LSTMGates/LSTMState touch exists before it returns -
loudly, since the failure it replaces was indistinguishable from a healthy
net that simply never fires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:39:14 -04:00
AnimateDread
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 a142749, so
   "CONVERGED ... balanced accuracy 32.5%" was reporting a 32.5%
   PRECISION as if it were macro-recall, while the same era logged an
   actual balanced accuracy of 49%. Two different numbers under one
   name, in the line that announces a deploy. Relabelled at every site,
   including the stage-3 refusal, which still described the per-class
   recall floor that stopped being the gate.

2. Precision is now bucketed by confidence tier and logged per era,
   both per-tier and cumulatively from each tier upward:

     | tier prec T0:19%(410)[>=28%/1204] T1:31%(520)[>=34%/794] ...

   The per-tier number says whether confidence is calibrated to
   correctness at all; if it does not rise T0->T3, raising the floor
   buys nothing and that is the finding. The ">=" number is what a floor
   would actually deliver, with its fire count, so the coverage cost is
   visible in the same line. Tier weights are 25/50/75/100, so for an
   AI-only config Min_Vote_Open maps straight across: 50 = ">=T1",
   75 = ">=T2", 100 = ">=T3".

Bucketing happens at the existing live-fired accounting site, so it
measures exactly the population that trades - not the raw argmax.

Both builds compile 0 errors, 0 warnings. No retrain needed for either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:47:15 -04:00
AnimateDread
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>
2026-07-30 10:40:36 -04:00
AnimateDread
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>
2026-07-30 10:05:40 -04:00
AnimateDread
3bc551b6e1 feat(nn): derive conv filter count and LSTM hidden size from the data
Same defect the first-layer width had before 2026-07-29: both were
inputs whose defaults were fixed constants picked with no reference to
the input they sit on, which is the only thing that decides whether
either number is sane.

The conv layer is a per-bar projection - AddConvStage sets
window = step = one bar's features - so its filter count should be read
against the per-bar feature count. Sixteen filters COMPRESSED a
50-feature configuration 3x but EXPANDED a minimal 4-feature one 4x, and
the expanding case adds parameters below every learnable layer without
adding information. Now derived as half the per-bar feature count,
snapped down a power-of-two ladder.

The LSTM stage was the bigger miss. Its weight count is exactly
4*H*(H+inputs+1) (CNeuronLSTMOCL::SetInputs) and AddLstmStage feeds it
the whole flattened vector, so the shipped 32 units against a 540-wide
input is ~73k weights - more than DOUBLE the entire derived dense taper
it feeds. It was the one stage the capacity budget never covered, which
is why deriving the dense stack alone did not stop LSTM and HYBRID from
being over-parameterized. Now solved from the same
one-weight-per-in-sample-bar budget the first layer spends.

Factored EstimatedInSampleBars() out of ComputeFirstLayerWidth so all
three decisions spend one budget rather than each guessing at the
training-set size separately. Both new values are assigned alongside the
first-layer width, before the fingerprint that hashes them, and are
functions of inputs already in that hash - so they need no entry of
their own, and the same reasoning removes them from the DB config key.

Both builds compile 0 errors, 0 warnings. Re-keys existing models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:22:11 -04:00
AnimateDread
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>
2026-07-30 09:05:58 -04:00
AnimateDread
16632c1c3c feat: make batch normalization mandatory, and record the run-3 results
EnableBatchNorm and BatchNormWindow demoted from inputs to constants. Batch
norm is required, not optional: measured on identical MLP_3L topologies it
was worth +11.3 points of balanced accuracy (57.0% with, 45.7% without),
stable across 150+ and 200+ eras, and the no-BN control converged to ~5% IS
and OOS accuracy with no chart signals at all. A user cannot make a good
decision here and can easily make a ruinous one, so the choice is not
offered. BatchNormWindow goes with it - a running-statistics window in
samples has no meaningful setting a trader could reason about, and its only
other reachable state (<=1) silently disables the layer.

Kept as named constants rather than deleted: the topology builder, the
weights fingerprint and the .cfg guard all read them, and a constant keeps
those paths - and the ability to flip one for a diagnostic rebuild - intact.
Fewer knobs also means a shorter Market description and less room for a
buyer to misconfigure.

EXPERIMENTS.md records runs 2 and 3, since the MT5 logs are wiped between
runs and these measurements are what the design decisions rest on. Run 3
(12h, uncapped tau=1.0) is a write-off: zero eras out of 1,993 across the
five batch-norm charts ever called a direction on fewer than half of all
bars, at a median precision equal to the ~6.1% base rate. The damage was
present at era 1 and never recovered over 292-766 eras.

Both builds compile 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 08:51:10 -04:00