Commit graph Warrior_EA/Expert/AIBase
Author SHA1 Message Date
AnimateDread
1a900a0a35 feat(vote): CONSENSUS arithmetic - agreement is now what the threshold dials
Era-680 report, all three observations one equation: "peak 29, no arrows at
threshold 30" / "at 20, arrows on EVERY bar" / "label at 12 while arrows
everywhere". Under the voters-only divisor, any bar with at least one
directional voter read the weighted mean of the firing tiers' weights - and
once the tiers self-ranked to each model's pooled win rate (~28-31), that
mean was NEAR-CONSTANT regardless of headcount. One member alone: ~29. Four
unanimous: ~29. Min_Vote_Open was a step function around that constant -
above it nothing ever fired, below it everything did - and the label's 12
was a 3v1 split netting through the same divisor. Not three display bugs:
one arithmetic that could not express agreement.

The divisor is now the CAPABLE weight - every filter that could vote,
whether it did or not:
 * live (Direction): VoteCapableWeight() - classic pattern ladders always,
   veto filters never, AI members once past the same readiness test
   LongCondition gates on. A model still training must not dilute an
   ensemble it cannot join: four trainees + one deployed model is a solo
   chart wearing an ensemble label, and the solo vote reads full strength.
 * gate (EnsembleEraVerdict): g_ensVoteWeightSum accumulates for every
   member that EVALUATED the bar, Neutral included.
 * overlay sweep + prospective readout: weight counts whenever the member
   has data; a snapshotted Neutral dilutes.
One arithmetic, four sites, same numbers everywhere.

What the numbers become (four members, w~0.29, tiers~29): unanimous ~29 -
the CEILING, which is the pooled win rate and is what the peak displays;
3-of-4 ~22; 2-of-4 ~14.5; 3v1 ~14.5. Min_Vote_Open 20 now means "roughly
three-quarters of the ensemble's trust agrees, net". It MUST sit below the
ceiling to ever fire - the census/peak states the ceiling.

This is the ensemble the user specified in the original design discussion
("if the perceptron also votes, both together reach the threshold; if
another NN votes the other side, the threshold is not reached") - union
semantics was the pre-ensemble behaviour, kept until measurement showed its
vote magnitude was a constant.

Plus overlay DECLUSTERING, the other half of "arrows on every bar": the
same three NMS rules as the per-member arrows (same-direction runs collapse
to their first bar, cross-direction flicker keeps the stronger side), online
over the sweep's strictly oldest->newest walk. Suppression is a verdict and
deletes a standing arrow; the den==0 no-data skip still never does.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 06:58:03 -04:00
AnimateDread
c393497fd6 fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era
Full-pipeline analysis after "threshold 30, attained often, nothing drawn,
still glued to buy". The log falsified the premise before any code did:

  21:40:43  swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0%
  21:42:07  swept 4999, 0 voters,  drew 0
  21:51:30  swept 4999, 0 voters,  drew 0
  21:56:30  swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0%

The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root
cause, three symptoms: every display path read m_arrowSignalCache, which is
wiped to sentinel at each era start and only complete again when pass 3
finishes. With eras at ~30s and a sweep at ~17s:

 * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its
   else-branch deleted the arrow on every voteless bar - erasing the previous
   sweep's entire output. The chart cycled populated -> blank -> populated;
   the user kept catching the blank phase.
 * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every
   era and fell through to dPrevSignal - the frozen purge-band edge bar that
   reads Buy. 659638e fixed which bar was frozen, not the freezing.
 * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
   different fraction of half-rebuilt caches.

THE FIX, structural rather than another patch:

1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
   moment the cache is complete - and now copies it (raw signals, newest
   LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
   BEFORE its early return: an all-Neutral era is a snapshot worth showing,
   not an absence of one. Raw signals rather than votes, so a tier re-rank
   between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
   snapshots; the readout's fallback chain is live-cache -> snapshot ->
   dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
   sub-threshold vote takes an arrow down. This alone ends the wipe half of
   the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
   i.e. precisely when caches are about to be wiped) to
   g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
   snapshot just got fresher", the only event a redraw can act on. 60s rate
   limit collapses the four members' burst into one sweep. Classic-only
   charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
   sell)" - so "the vote leans buy" is checkable from the log instead of
   inferred from arrow colours.

Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
AnimateDread
e59dc1629f fix(deinit): vote arrows survived the cheap sweep, and 5 long loops ignored the stop
Leftover chart objects on long-history charts. Two causes, one of them
introduced by 07aa017.

THE ONE I ADDED. The filtered view's overlay draws up to
SIGNAL_RESCAN_LOOKBACK_BARS vote arrows. OnDeinit's EARLY VISIBLE-UI SWEEP
runs with skipArrows=true, which skips any prefix equal to SIG_ARROW_PREFIX -
and "WarSig_VOTE_..." starts with "WarSig_", so every one of them was skipped
by the one sweep that is cheap enough to always complete. They then sat in the
object list while the two expensive scans that follow walked it: a per-member
SaveChartSignals O(total) scan, then the by-name rescan. On a chart with years
of history that is thousands of extra objects walked twice, inside a teardown
budget measured from the stop REQUEST rather than from OnDeinit's first line.

skipArrows exists because the per-model arrows' sidecar is rebuilt by SCANNING
them off the chart, so they cannot be deleted before that write. Vote arrows
have no sidecar - they are a reconstruction, rebuilt on the next attach - so
nothing is preserving them and they now get their own prefix slot, deleted by
one native call in the first few milliseconds.

THE FIVE LOOPS. A time budget bounds THROUGHPUT, not latency to an unload, and
OnDeinit cannot begin until whatever is in flight returns. These all scaled
with history and none of them checked:
  * Training passes 2, 2.5 and 3 yielded only on TRAIN_TIME_BUDGET_MS. Pass 1
    has checked IsStopped() all along; the other three never have, and they
    are the ones that grow with the bar count. Free to fix - the resume state
    is written either way, so a stopped chunk simply is not re-entered.
  * PruneDirectionalClusters: the one UNCHUNKED sweep left, once per era over
    every bar, with its own header noting that raising the training budget
    cannot help its cost. Now bails outright.
  * AdvanceChartSignalRestore / AdvanceChartSignalRescan: chunked, but the
    rescan runs a full feedForward per bar over up to 5000 bars and the
    restore can hold MAX_RESTORED_ARROWS entries. Checked on the same
    64-object stride as the clock read, since the check is not free either.
  * AdvanceFilteredOverlay (mine, 07aa017) replays Direction() on every
    classic filter per bar and had no check at all. Now per bar.

ChartUI.mqh had ZERO shutdown checks across six loops before this.

Nothing was added to the purge path itself: that is the work that must
complete, and an IsStopped() check inside it would abort unconditionally -
IsStopped() is already true by the time OnDeinit runs.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 17:13:50 -04:00
AnimateDread
b28c81eb78 feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat 4858507 shipped with: the vote is a confidence percentage,
but only to the extent the pattern weights are measured. AI tier weights sat
at their designed defaults (25/50/75/100) because AI rows only ever arrive
from LIVE journaling, of which a training run produces almost none.

AND A STALE-TIER BUG THAT MADE THE EVIDENCE MEANINGLESS. The OOS scan bucketed
every scanned bar by ConfidenceTier(), which reads dPrevSignal - and
dPrevSignal is assigned in PASS 1 only, never anywhere in the OOS scan. So an
entire era's fires were bucketed by one stale, unrelated bar's confidence and
landed in a SINGLE tier. That is the "tier prec T0:72%(828) T1:n/a(0)
T2:n/a(0) T3:n/a(0)" symptom recorded on 2026-08-16 and attributed to the
calibration clamp. The clamp was real and was fixed then; this is a second,
independent cause of the identical output that survived that fix untouched -
which is why the log kept reading the same afterwards. Two causes, one symptom.
Now ConfidenceTierFor(adjSig): the bar this iteration actually scored.

WHY THIS DOES NOT WRITE ROWS TO THE SIGNAL DB, which was the obvious reading of
"fill the database during training". The user's own observation is the reason:
a classic Pattern_2 is a fixed geometric condition, so its win rate is
legitimately accumulated over years, but an AI Pattern_2 means "confidence
landed in tier 2" and tier 2 under era 100's weights is a different statement
from tier 2 under era 500's. The DB's value is ACCUMULATION, and accumulation
is exactly what is wrong here - it would average together models that no
longer exist, while colliding with the per-table row cap and mixing
measured-on-holdout outcomes into the live ledger's own tables. What the DB
actually supplies is a measured win rate per pattern, and pass 3 already
computes that on held-out bars, thousands at a time. So the model ranks itself
once per era, REPLACING rather than accumulating, which makes the weights
describe the current weights by construction.

ESTIMATOR. Not WinRateFromCounts(): it returns NO_DATA below 100 raw trades
BEFORE shrinking, which here would fire on every tier every era and hand all
four the pooled rate - the tiers could never separate and the mechanism would
be inert. Shrinkage is the answer to a small sample; a floor in front of it
means the shrinkage never runs. Instead: a Beta prior of TIER_PRIOR_EFF_N
pseudo-observations centred on the model's pooled holdout rate, counted in
EFFECTIVE observations, because overlapping triple-barrier labels mean 800 raw
fires can be worth ~12 independent ones. Rounded to the integer, not to the
decade NormalizeWinRate() uses, which would collapse the shrunk tiers back
into one number.

NO SAME-ERA CIRCULARITY, and it falls out of the ordering rather than a guard:
weights are computed at the END of era N, so the vote scored during era N was
cast with era N-1's weights. The deploy gate never grades a vote whose weights
were fitted on the bars it is scoring. Residual leakage remains - the same OOS
bars each era under a different model - and is stated in the code rather than
papered over.

Both DB clobber paths are closed: ApplyPatternWeight() declines once
self-ranked, and UpdateSignalsWeights()' filter.Weight() call is guarded by
SelfRanked() - guarding only the tiers would have let the hourly ranking pass
undo half the self-ranking.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:00:32 -04:00
AnimateDread
4858507146 feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."

WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.

Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.

ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
  * the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
    reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
    classic side, which is the only reason that route exists - against the
    same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
    retired rather than left dangling.
  * m_oosDecisionSeries now carries the vote, not the confidence, so the exit
    SIMULATION stops modelling a close rule the EA does not run.
  * ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
    through that would have silently switched vote exits off in the
    simulation while live went on running them - found before it shipped;
    the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.

CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.

Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:52:08 -04:00
AnimateDread
282b535037 feat(chart): filtered view - one arrow per trade the bot would actually take
Adds DrawUnfilteredSignals (default OFF) and, with it off, replaces the
per-model arrow layer with the decision the EA would really have made.

THE FILTERED ARROW IS DRAWN AT THE ORDER, NOT AT THE THRESHOLD. Clearing
Min_Vote_Open is not the same as trading: a setup can pass the vote and still
never reach the broker (invalid SL/TP, stops-level, ATR warm-up, unsynced
swing history), and every one of those lands in OpenParams' failure branch.
So DrawVoteArrow() fires only after the order parameters validate, and the
failure branch withdraws any arrow already standing on that bar. One arrow is
one entry the EA would have placed - carrying the vote, the threshold it
cleared, and the SL/TP the order would have had.

Classic signals now draw too, under their own name and weight, so a chart
running MA/RSI/MACD/Ichimoku alongside the nets reads the same way an
ensemble chart does. They can only be drawn from the aggregate's once-per-bar
pass, because unlike the AI members they have no cached per-bar scan.

Two subtleties that would each have produced a quietly wrong chart:
- The raw classic draw sits AFTER filter.Direction(), not beside the
  journaling block. GetActivePattern*() are CONSUMING reads holding the
  PREVIOUS evaluation - "one tick later", which at Expert_EveryTick=false is
  one BAR later. Keyed off those and placed at StartIndex(), every classic
  arrow would have been drawn one bar early, which on a chart is
  indistinguishable from a model that genuinely leads. Peek*() accessors
  (non-consuming) let pattern, weight and bar come from one evaluation.
- CExpertSignalAIBase::DrawObject() early-returns instead of gating its five
  call sites, so the switch cannot be honoured in three passes and missed in
  the fourth. Its delete counterparts stay ungated so flipping the input off
  and rescanning clears the raw layer rather than stranding it.

SIG_ARROW_PREFIX and g_signalsVisible move from ExpertSignalAIBase.mqh down
to ExpertSignalCustom.mqh - the nearest common ancestor - because the classic
signals cannot see the AI header (it is included later in Warrior_EA.mq5).
The vote layer gets SIG_VOTE_PREFIX under the same bare prefix, so
WarriorChartPrefixes()' purge still reaches every arrow without knowing they
exist.

NOT YET BUILT: the reconstructed history behind attach. Filtered arrows
currently start where the EA starts. See the next commit.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:41:28 -04:00
AnimateDread
2c443ba3ad fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:

CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.

DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.

LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.

ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.

NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.

Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
AnimateDread
525e92ecb2 fix(plateau): the IS-error early stop was inert for every ensemble member
15 hours of training, and the stop that exists to END a run announced itself
1,299 consecutive times without ending anything:

  SP500 ConvLSTM  IN-SAMPLE ERROR PLATEAU - not improved in 1297 / 1298 / 1299
                  eras (best 0.2689, now 0.3269) ... era 1396, 1397, 1398
  SP500 LSTM      536 eras     SP500 CONV  442 eras     SP500 PAI  150 eras
  XAUUSD HYB      478 eras     XAUUSD LSTM 296 eras     XAUUSD CONV 366 eras

CAUSE: it wrote its decision into m_plateauStage, and EnsembleEraVerdict mirrors
the shared ladder onto every member - `mm.m_plateauStage = g_ensPlateauStage` -
on EVERY era, purely so each member's status line shows the collective stage. A
display mirror was silently overwriting a decision, so the stop re-armed and
re-fired the next era, forever.

This is the worst possible direction for this particular bug. Every one of those
1,299 eras was scored out of sample and joined the family the deploy gate
corrects over (Sidak, g_ensCandidateEras). The stop's entire purpose is to make
that family SMALLER; instead the run spent fifteen hours raising its own bar.

- m_isErrorPlateaued: a one-way per-member latch, cleared only by a fresh run.
  Nothing in the ladder may reset it. The stop condition and the two solo deploy
  conditions read the latch, not the mirrored stage.
- The orchestrator combines: EnsembleEraVerdict requires UNANIMITY across
  participating members (same participation test the era barrier uses, so an
  excluded or finished member cannot veto). One member still learning can still
  move the combined vote, and the vote is what the gate certifies.
- Fed in as `dueStage = PLATEAU_STAGE_DEPLOY`, NOT written to g_ensPlateauStage.
  The block that actually ends the run sits under `dueStage > g_ensPlateauStage`,
  so assigning the stage directly makes that test false and the deploy never
  happens - the same inert-write shape as the bug being fixed. Caught before
  committing; raising dueStage carries it through the ladder's own path (warm
  restarts skipped, family-wise vote test, measurement screen, joint checkpoint)
  unchanged.
- g_ensIsPlateauAnnounced: announce once per run, not once per era.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:09:26 -04:00
AnimateDread
65a3e4e877 fix(chart): a purge that reports "zero leftovers" was only ever checking its own list
2026-08-17 21:58: all three charts hit "Abnormal termination" ~5.3 s into
OnDeinit with NO cleanup-timings line - the teardown was starved again. The
22:00 init purge then removed 993 / 1373 / 1557 stranded objects and reported
ZERO by-name leftovers on every chart, and the charts still came up with
duplicated panels. "Nothing matching our prefixes remains" and "the chart is
clean" are different statements and only the first was being made.

Three changes, in the order they matter:

1. WHY the teardown starved, and it is a gap in ad80e0b. StartLabelCachePrebuild
   runs ResizeBuffers + RefreshData over the FULL study window (33,984 bars on
   XAUUSD), unchunked, and OnDeinit cannot begin until it returns. Normally a
   once-per-run cost. That night SP500 and XAUUSD LSTM were wedged in the "cache
   invalidated at era start" loop, which calls it on EVERY Train() call - two
   members re-preparing tens of thousands of bars indefinitely. The terminal
   closed into that. Guarded now, plus a resumable guard in the prebuild chunk
   loop (the tally pass after it is not chunked).

2. Catch-all "Warrior" prefix in WarriorChartPrefixes. Every family this EA
   creates is named Warrior* except the arrows (WarSig_), so one bare prefix
   covers the three named entries AND anything a rename or a stale .ex5 left
   under a name nobody remembers. Still a prefix delete, never
   ObjectsDeleteAll(chart) - the user's own drawings are not ours to remove. Does
   not defeat skipArrows: "WarSig_" does not start with "Warrior".

3. The init purge now REPORTS the residue it did not claim, by name (up to 12).
   Not deleted - an unmatched object may belong to the user or another indicator.
   If a Warrior panel is visible and appears in neither the removed count nor
   this list, the prefix list has drifted a third time and the name is in the
   journal instead of being inferred from a screenshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:07:09 -04:00
AnimateDread
64b77e4bbe fix(diag): the cache-invalidation stall message could never name the cause
SP500 and XAUUSD LSTM wedged at era 1 from 19:43 to 21:00+ (77 min) while their
three siblings passed era 200 - the 12-minute barrier exclusion correctly kept
the charts alive, so the ensembles ran three-handed. Both printed:

  cache invalidated at era start (era sized 16236 bars, cache holds 16236)

Equal numbers, which reads as "so it wasn't the size". That inference is not
available: EnsureBarCachesCapacity assigns BOTH invalidation keys
(m_labelCacheBars = bars, m_labelCacheAnchorTime = m_Time.GetData(0)) before it
returns true, so a message built afterwards reports the values it just
overwrote. The two counts are equal BY CONSTRUCTION and ReportTrainStall's
anchor= field is always the live one. The line whose stated job is to name which
key tripped was structurally incapable of naming it.

Capture bars/anchor BEFORE the call and say which one moved:
  "SIZE CHANGED 16236 -> 16240" or "size unchanged", and
  "ANCHOR MOVED 2026.08.18 00:00 -> 04:00" or "anchor unchanged".

Not guessing at the cause. An anchor moving every era with the size steady is a
new candle each pass or a Time buffer that is not being refreshed; a moving size
is the era/prebuild disagreement the branch was written for. The next occurrence
will say which, instead of costing another session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:57:12 -04:00
AnimateDread
f102a695d5 fix(geometry): the ensemble was training on TWO DIFFERENT TARGETS - propagate the adopted barrier
MEASURED 2026-08-17 19:06 on USDJPY, in the fresh run:

  19:06:38  LSTM  adopting barrier geometry 2:10 ... geometry authority
  19:06:40  LSTM  triple-barrier labels - stop 2.00 target 10.00, horizon 256
  19:06:44  PAI / CONV / HYB   break-even 33.3%, mean label lifespan 19.2 bars
  19:06:45  LSTM               break-even 16.7%, mean label lifespan 81.4 bars

One chart, four members, two targets. A "Buy" from LSTM meant "10 ATR before a
2 ATR stop within 256 bars"; a "Buy" from PAI meant "3.21 before 1.61 within 64".
The orchestrator averages those votes and the joint gate certifies the average as
though they answered one question. And g_DerivedSlAtrMult - which places the LIVE
order - is a single global, so the stop actually sent was whichever member wrote
last: the same last-writer-wins class of bug as the live-exit confidence.

CAUSE, and it is mine. The geometry scan sits at the end of the MI chain, and
that chain runs ONCE PER CHART (g_ensembleChartMiReportDone) - whichever member
reaches it first measures and the rest skip. Harmless while the scan only
PRINTED; 62a719f made it authoritative and turned a skipped report into a
skipped DECISION. The indicator tuner already had this doctrine
(g_ensembleChartTuneSettings); the geometry had no equivalent.

- g_ensembleChartGeomAdopted/Sl/Tp/SlMode/TpMode: the donor publishes its
  pairing, the siblings adopt it in the MI-skip branch. Ordering is safe by
  construction - MQL5 is single-threaded per chart and the donor sets
  g_ensembleChartMiReportDone only after the chain (and so the adoption)
  returns, so any member taking the skip branch does so strictly afterwards.
- ApplyAdoptedGeometry(): the eleven side effects an adopted pairing must carry
  - derived pair, legacy mode ints, g_Derived* live globals, .cfg rewrite, label
  cache invalidation, horizon unlatch - in ONE function, because there are now
  two callers and duplicating them is how the two paths drift.
- Guarded on era 0 for the donor's own reason: relabelling a partly trained net
  moves the target out from under weights already fitted to the old one.

STILL OPEN: dead MA handles were not eliminated by cb30360. They now appear at a
different site (SP500 19:06:35, during "label prebuild", and on PAI - the member
that RAN the sweep), so there is a second handle-churn path I have not found.
Recovery works and the sharing diagnosis stands; the trigger is not only the
tuner's adopt branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:35:45 -04:00
AnimateDread
cb30360c18 fix(handles): the MA handle was SHARED, and a rejected sweep freed it for everyone else
ROOT CAUSE of the six-session "silent block failure", measured rather than
inferred. All TWELVE dead-handle recoveries in today's log report the SAME
handle number - MA=-1(h13) - across two charts and all four members. It was
never four handles. It was one.

MT5 refcounts indicator requests, so four ensemble members asking for the same
iMA on the same symbol/period share a single handle. TuneIndicatorsByFilter
creates and drops ~35 of them scoring candidates; the sweep's runner ends up
holding a live handle while its siblings still hold a number the terminal has
already freed. Timeline, twice, to the millisecond:

  USDJPY 18:10:03  PAI: auto-tune complete
         18:10:29.864/.910/.953  CONV/LSTM/HYB: "already ran ... REJECTED"
         18:10:30.057/.065/.074  all three: MA=-1(h13), sweep bars all rejected
  XAUUSD 18:10:11 -> 18:10:42.19/.23/.27 -> 18:10:42.334  identical, same ~100ms

The adopt branch re-initialised indicators only `if(g_ensembleChartTuneInstalled)`
- exactly backwards. A REJECTED sweep churns just as many handles, and every one
of the twelve recoveries followed a rejection. Parameters are still adopted only
on an install; the HANDLES are now rebuilt either way. Four creations per chart.

RepairDeadIndicatorHandles stays - it is cause-agnostic and it is what made this
diagnosable. This removes the cause it was recovering from.

Also, consistency of the warm-up status (user-reported: "only one nn will say
scoring indicators, which leaves some doubt about what is going on"):
- the sweeping member now says it is scoring "for the whole chart", so three
  idle rows read as the design rather than a stall;
- the two adopt branches (tuner and MI) publish to the panel instead of only
  printing, so every row accounts for itself;
- the MI suite publishes before it runs. It is the longest stretch of the whole
  warm-up - MI, lag profile, excursion targets, geometry scan, each with its own
  permutation null - and it published nothing at all, so during most of the
  warm-up the panel's last word described a step that had already finished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:16:28 -04:00
AnimateDread
ad80e0bb57 fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks
Two things, one of which was a compile error.

1. ExitPolicy() was declared in the protected block but is pushed in from
   Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters.

2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured
   from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot
   begin until whatever is in flight returns - so a scan still running after
   _StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge
   never gets its turn.

   New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress.
   Deliberately NOT m_trainingStopRequested: that latches, and a latched flag
   would permanently disable scans that must run again on the next Start.

   Guarded, longest first:
   - TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings
     on the way out (best[] is mutated in place; the tuner otherwise keeps the
     last trial's parameters, which nothing chose).
   - ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so
     m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar
     read the last candidate's multiples as the configured geometry).
   - ReportFeatureLabelInformation / ReportExcursionInformation / lag profile -
     nulls ABANDON rather than truncate: fewer draws is not a smaller null, it
     is a wrong one, and p shifts toward significance. m_dirEvidence staying
     false is the safe direction.
   - SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line
     is dropped instead of latching a partial expectancy as the run's only report.
   - ReportGeometryExpectancyScan - per ladder rung.
   - HttpGet - one choke point for up to a dozen blocking WebRequests per
     first-pass Update(). An in-flight request cannot be cancelled; refusing to
     start another is the whole remedy.
   - PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain -
     entry points, so a queued event cannot open an era during teardown.
     TuneIndicatorsAndTrain's guard is the first statement, ahead of the
     m_tuneFilterDone / g_ensembleChartTuneDone latches.
   - OnTick / OnTimer / OnChartEvent.

   Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3
   yield on a 120 ms budget); the warm-up scans did not, and they are the longest
   uninterruptible stretches the EA has.

   StopTraining() is unchanged: the operator's Stop still finalises synchronously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
AnimateDread
dbb3d7fcd8 fix(geometry): do not adopt a pairing that cannot be certified - information and detectability are different objectives
Closes a gap 62a719f opened. Making ReportBarrierGeometryScan authoritative put two
objectives in charge of one decision without reconciling them:

  - the scan maximises entry-time INFORMATION, in nats;
  - the deploy gate needs enough INDEPENDENT observations to certify an edge.

They pull opposite ways. A wider pairing carries more information per call AND takes
longer to resolve, and overlapping labels are worth ~1/L each - so tripling the
horizon divides the independent sample by ~3 and multiplies the standard error the
gate has to beat by ~sqrt(3). USDJPY's current winner is exactly that trade: 2:8 at
h192 against an incumbent 1.61:3.21 at h64. Until today the adoption was inert so it
never mattered; from 62a719f it decides the geometry, and it would have made that
swap silently on the next fresh run.

The guard inverts the deploy identity for the WINNER's own pairing - certifying an
edge d needs z^2 p(1-p)/d^2 independent calls, at that pairing's break-even - and
compares it against what the OOS window can physically supply at that horizon. If
even a generous 10pp edge is out of reach, the pairing is not adopted and the log
says it lost on detectability rather than on information.

Conservative by construction: the horizon is an UPPER bound on the mean label
lifespan, so oosBars/horizon is a LOWER bound on available independent observations.
The guard therefore only fires when the pairing is hopeless, never merely hard.

This is the same quantity ReportDetectability publishes per run, applied at the one
moment it can still change a decision instead of after the geometry is already
pinned. More information per trade is worth nothing if it buys too few independent
trades to prove it - WIDTH is not free, and on this window it is the binding
constraint, not the nats.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:47:37 -04:00
AnimateDread
62a719f04c fix(consistency): one geometry authority, one exit authority, and the MI screen finally gets a veto
Consistency pass before a fresh deployment. Three places where two systems were
choosing the same thing and one of them silently lost.

1. THE GEOMETRY SCAN'S DECISION WAS INERT - measured, not suspected.

USDJPY, 2026-08-17:

  14:24:12.844  adopting barrier geometry 2:8 ... Relabelling and training on it.
  14:24:12.979  triple-barrier labels - stop 1.61*ATR, target 3.21*ATR ... this is what trains

It adopted 2:8 and trained on 1.61:3.21. ReportBarrierGeometryScan wrote only
m_sl_mode/m_tp_mode, and BarrierMultiples ranks the DERIVED pair ABOVE those ints -
so on any model carrying a derived pair (every model with a .cfg, including a fresh
one whose weights are gone but whose sidecar survived) the adoption changed nothing.
Worse, had it changed something it would have been undone immediately: the adoption
sets m_labelCachePrebuilt = false, and that prebuild re-runs DeriveBarrierGeometry at
era 0, which overwrites m_derivedSl/TpMult from the excursion quantiles.

ONE AUTHORITY: the derived pair, because it is what the labels read, what the deploy
gate certifies, what g_Derived*AtrMult places on the live order, and what the .cfg
pins across restarts. The scan now writes THAT (floored by MIN_SL_ATR_MULTIPLIER, the
same floor DeriveBarrierGeometry applies so the live stop can never be wider than the
labelled one), republishes to the bridge immediately rather than at the next era end,
and forces the sidecar to be rewritten. m_geometryAdopted latches it so the derive
pass the adoption itself triggers cannot overwrite it.

The scan outranks the derive for an evidential reason, not an architectural one: its
winner cleared a permutation test against the null of the MAXIMUM over every eligible
pairing, and it scores the incumbent derived pair as a peer in that same field. The
derive is a descriptive quantile read with no significance test attached.

BEHAVIOURAL CHANGE, and the reason to flag it before a fresh test: barrier geometry
will now actually move when the scan says so. Until today it never did.

2. ONE EXIT AUTHORITY, tied to whose certificate the trade was placed under.

CheckClosePosition had two routes. The AI early-exit reads the AI vote undiluted and
is exactly what the new exit replay reproduces. The blended route thresholds
m_direction, the average over EVERY filter including classic ones whose live votes
pass 3 never computes - so it can close a position the certificate never modelled,
and no replay can ever check it.

When g_DerivedSlAtrMult > 0 the AI's measured geometry is on the order, which means
the deploy gate's certificate is the reason the trade exists. In that state the AI now
governs the exit and the blended route is suppressed. Classic-only configurations are
untouched: there the blended route is the only exit opinion and stays exactly as it
was. Nothing moves at the shipped defaults either way (Min_Vote_Close = Disabled).

3. EDGEFINDER, SECOND HALF: THE MEASUREMENT NOW STEERS.

The MI suite has always printed its verdicts and then trained the direction target
regardless of what they said. That gap IS the difference between this and the
EdgeFinder discipline: measure what the market offers, THEN aim.

m_dirEvidence is set when EITHER the feature/label mutual information OR the
normalised excursion asymmetry clears its block-permuted null - an OR, because the two
look for the same thing by different routes and requiring both would reject on the
weaker of two independent measurements. Normalised asymmetry specifically, never the
raw one, which is the volatility confound.

Deploy - solo AND ensemble - now requires it. A run without it still trains, and keeps
its checkpoint: the research value is real and the measurement can be wrong. It simply
may not go live. Reported separately from the statistical gate because the remedy is
different: a failed selection test says train differently, this says look somewhere
else. Excursion SIZE keeps clearing where direction does not, and that is a
risk-control head rather than an entry signal.

For the ensemble the check is per-chart by construction - the MI suite runs once and
shares its outcome across members - which is the honest treatment: four models finding
nothing between them is not four chances at an edge, it is four fits to the same
absent information.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:43:20 -04:00
AnimateDread
6069581323 feat(search): stop on the IN-SAMPLE plateau, and shrink every best-of-K effect before quoting it
Points 3 and 4 of the four-point plan.

1. IN-SAMPLE EARLY STOP - and the reason it is worth having is not compute.

The plateau ladder stops on the OOS SELECTION score. That is a peek: by the time it
fires, every one of those eras has been evaluated out of sample, so all of them sit
in the family the deploy gate corrects over (g_ensCandidateEras, Sidak). Training
longer therefore does not merely cost time - it RAISES the bar the eventual winner
has to clear.

The new stop reads the TRAINING error, which the gate never looks at. When the
optimiser has stopped improving on data it can see, more eras will not find a better
model; they will only enlarge the OOS family. Ending there shrinks the correction,
and the shrinkage is legitimate precisely BECAUSE the stopping rule never consulted
an out-of-sample number.

That distinction is the whole point and it is the one this project has got wrong four
times: stop on IS and the family really is smaller; stop on OOS and those eras were
searched and still count. Both stops now exist; only this one buys a lower bar.

Deliberately more patient than the OOS ladder (IS_ERROR_PATIENCE_MULT = 3x): training
error is noisy per era - mini-batch order alone moves it - and ending a run that is
still learning costs far more than a few wasted eras. Improvement is RELATIVE
(IS_ERROR_IMPROVE_FRAC = 1%), so it does not depend on the loss's absolute scale, and
it only acts when a checkpoint exists, since otherwise it would end a run with
nothing to deploy. Reset per RUN alongside the ladder, so a resumed run cannot
early-stop on its first era against a previous run's best.

2. WINNER'S-CURSE SHRINKAGE ON THE BARRIER-GEOMETRY WINNER.

The family-wise permutation gate already establishes that the RANKING is not noise.
It says nothing about the SIZE of the winner's effect - and a best-of-K maximum is
biased upward by construction, being the largest of K noisy draws. The adoption
message quotes that raw maximum and compares it against the incumbent, so the number
a reader plans on is the inflated one.

The penalty is now measured, not assumed: the same permutation draws that produce the
p-value also produce, per draw, the MAXIMUM excess across all candidates under pure
noise. The mean of those maxima is exactly what a best-of-K selection is expected to
report when there is nothing there. This is the empirical form of the sqrt(2 ln K) x SE
penalty the SQX EdgeFinder plugin applies to every maximum it reports (Stats.java:79-88),
and it needs no normality assumption because the draws ARE the null distribution.
Applied in James-Stein form - effect x max(0, 1 - penalty^2/effect^2) - so a large
effect is nearly untouched and a marginal one collapses toward zero.

Reported, not gated. The adoption decision still turns on the permutation p-value,
which is the right test for "is the ranking real"; the shrunk number is there so the
magnitude quoted beside it is one worth planning on. Closes the first of the two
EdgeFinder ports identified on 2026-08-12.

NOTE on the second EdgeFinder port, deliberately not done here: "let the measurement
steer the target" is already true where it matters most - ReportGeometryExpectancyScan
ADOPTS the winning barrier geometry under the family-wise gate rather than advising
it, and the MI excursion suite publishes a verdict per instrument per config. What is
still missing is steering the TRAINING TARGET itself (direction vs excursion) off
those verdicts, and that is a design change rather than a surgical one - direction is
a closed verdict while excursion SIZE keeps clearing, so the honest version of that
change is a target-selection policy, not a flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:29:00 -04:00
AnimateDread
94019f363e feat(gate): grade OOS calls on the exit policy actually in force, and move vote combining out of the members and into the orchestrator
Option (a) from the exit-policy question: the certified number must be the traded
number. Plus the modularity correction the user called for on 778b6c0.

1. VOTE COMBINING BELONGS TO THE ORCHESTRATOR, NOT TO A MEMBER.

778b6c0 fixed the last-writer-wins bug on g_LiveAISignedConfidence by having a
member average its siblings through g_warriorEnsemble. That trades a scheduling bug
for a coupling bug, and it is the wrong shape for this EA: every signal runs in its
own instance, minds its own state, and VOTES to the orchestrator, which is the only
thing allowed to combine opinions.

Replaced with a publish/aggregate pair in Variables\ConfidenceBridge.mqh whose split
is enforced by shape rather than by convention:
  - PublishAIVote(slot, conf)  - a member writes ONLY its own slot, reads nobody's;
  - AggregateAIVotes()         - called by CExpertSignalCustom::LiveSignedConfidence.
CExpertSignalAIBase::EnsembleLiveSignedConfidence is gone. The orchestrator also
republishes the aggregate into g_LiveAISignedConfidence, because the intelligent
trailing reads that global directly and must act on the same number the exit route
does rather than on a leftover from whichever member ticked last. A solo AI signal
owns slot 0, so the non-ensemble path is unchanged.

2. THE GATE NOW REPLAYS THE REAL EXIT RULE.

SimulateTradeOutcome() walks the same price series with the same fill/barrier/spread
convention as ComputeLabelForBar - deliberately by copy, so a disagreement between
the two can only be a policy effect and never a discrepancy between two pieces of our
own arithmetic - and terminates at the FIRST of stop / target / vote reversal /
horizon. Barriers are tested before the vote on the same bar: intrabar we cannot know
which came first, and the barrier is what the broker executes automatically, so
checking the vote first would credit the exit policy with escapes a real stop would
have taken out of its hands.

It runs AFTER pass 3, not inside it. A vote-flip exit for a trade entered at bar r is
decided by the model's output at bars r-1, r-2, ... - NEWER bars - and pass 3 walks
oldest-to-newest, so at the moment r is graded its own exit does not exist yet. Only
once m_oosDecisionSeries is complete over the whole OOS window can the replay run.
In ensemble mode that series carries the member's adjusted decision and the live exit
reads the ensemble aggregate, which is the coupling the user identified: an LSTM entry
really can be closed by the ensemble turning against it.

3. THE STATISTIC HAS TO CHANGE WITH THE POLICY, AND THAT IS THE REAL FINDING.

A barrier exit pays a fixed R. A vote exit pays whatever the close happens to be. So
the moment vote exits are enabled the payoff is CONTINUOUS, and "win rate vs
break-even" stops being a meaningful test - there is no fixed break-even for a
variable payoff. SimulateTradeOutcome therefore returns R rather than a bool, and the
replay reports expectancy in R with its SE taken from the R distribution (overlap-
deflated on the same EffectiveSampleSize doctrine as every other SE here), not from a
binomial.

This is the same class of error as win-based scoring in 2026-08-09: measuring a
variable-payoff process with a fixed-payoff statistic. Naming it now, while vote exits
are still off, is much cheaper than discovering it after they go on.

4. WHY THIS IS SAFE TO SHIP TODAY.

Min_Vote_Close ships Disabled (101, unreachable on both scales it drives) and reaches
the AI signal through the new ExitPolicy() setter as 1.01, which the setter turns into
0.0 = no vote exit. Under that policy every replayed trade resolves at a barrier and
the simulation is arithmetically the same trade the deploy gate already certifies -
they cannot drift. The report says so explicitly, and prints ONCE per run in that
state; when vote exits are on it prints every era, because then the divergence is the
thing to watch. Nothing about today's numbers moves.

The gate switchover is wired but dormant by construction: it becomes exit-aware the
moment the input is enabled, which is exactly what "the certified number is the traded
number" has to mean.

KNOWN LIMIT, stated rather than hidden: only the AI early-exit route is replayed. The
rule-based averaged-vote close (m_threshold_close) depends on every other filter's
live vote, which pass 3 does not reproduce, so a position the classic filters would
have closed is held to its barrier here. The replay therefore holds LONGER than live
and overstates barrier-reached outcomes. Faithful only while the AI is the dominant
vote - which is the configuration this is being built for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:25:57 -04:00
AnimateDread
778b6c09c6 feat(gates): derive the recall floor instead of configuring it, publish what the configuration can PROVE, and stop an arbitrary member driving live exits
Three changes, all from the same principle: measure what is there before aiming
at it, and never certify a number you do not trade.

1. THE RECALL FLOOR IS DERIVED, AND IT MOVES BELOW CHANCE.

MinRecall=40 was a constant doing a statistical job. Its reference point is the
33.3% recall a zero-skill 3-class model gets on EVERY class, and against that the
constant was accidentally calibrated for exactly one sample size: on USDJPY CONV
(n_eff 195) 40% is chance + 2.0 SE; on SP500 PAI (n_eff 42) the same 40% is chance
+ 0.9 SE. One chart was being held to a bar twice as strict as the other, for no
reason anyone chose.

CollapseRecallFloorPct() computes it per class from that class's own effective
sample - EffectiveSampleSize(), so the overlap deflation the rest of the gates use
applies here too - as chance - EDGE_MIN_SIGMAS x SE. 26.5% at n_eff 195, 18.8% at
n_eff 42.

BELOW chance, deliberately, and this is the substantive change rather than the
arithmetic. This gate's only job is refusing to call a COLLAPSED model converged.
It is not a quality bar; the deploy gate is the quality bar and it is already
rigorous (chance + 2 SE on the deflated sample, Sidak over candidate eras, then the
cross-instrument pooled certificate). A convergence gate that ALSO demands
provably-above-chance recall on all three classes double-counts that job, and it
has failed that way twice here: MinRecall=60 blocked every SP500 H1 run in 2026-07,
and the 40 that replaced it made Neutral structurally unreachable once first-touch
resolution cut Neutral to a 0.65% residue. A floor nothing can reach does not make
a funded account safer, it stops the run converging at all.

Testing significantly BELOW chance instead catches what a fixed 40 was actually
catching - a model that has stopped emitting a class - and cannot become
unreachable by construction. It also fixes the direction the old constant scaled:
it now widens on a thin OOS window, where low recall genuinely cannot be told from
noise, and tightens on a rich one. Today's SP500 PAI (Buy 51 / Sell 18 / Neutral 30)
is still correctly blocked on Sell.

This also resolves a standing contradiction the code half-admitted at the
isBetterEra comment: selection ranks on coverage-weighted PRECISION while
convergence gated on RECALL, so a sparse high-precision abstainer - precisely the
model that could clear the deploy bar - was blocked by the floor.

The era line now PRINTS the derived floor. Anyone comparing these recalls against a
remembered "40" is reading the wrong bar.

2. DETECTABILITY: WHAT THIS CONFIGURATION COULD PROVE, BEFORE IT TRAINS.

The DEPLOY BAR line states the bar. It never said what reaching it would take, and
that is the actionable direction. ReportDetectability() inverts the same identity -
the gate passes when edge >= z x sqrt(p(1-p)/n_eff), so certifying an edge d needs
n_eff >= z^2 p(1-p)/d^2 independent calls, hence L times as many raw ones - and
prints a +2 / +5 / +10pp ladder as required independent calls, raw calls, and share
of the OOS window, marking any rung that needs more than the window holds IMPOSSIBLE.

Every term is a property of the CONFIGURATION - geometry via break-even, horizon via
mean label lifespan, window via oosCutoff - so no amount of training moves any of
them. It fires once, at the first healthy sweep, beside ReportFeatureHealth, for the
same reason: that is the first moment the bar grid, the measured geometry and the
lifespan are real numbers rather than defaults. It gates nothing.

This is the EdgeFinder discipline applied to our own gate: establish what the market
and the measurement design have to offer, then point the net at it - rather than
spending a thousand eras chasing something this OOS window could never certify.

3. AN ARBITRARY MEMBER WAS DRIVING LIVE EXITS AND TRAILING (user-identified).

Every ensemble member ran

    g_LiveAISignedConfidence = SignedAIConfidence();

unconditionally, every tick. Last writer wins. Its consumers are the AI early-exit
route (CExpertSignalCustom::LiveSignedConfidence) and TrailingIntelligent - so on a
four-model chart an LSTM entry could be closed, and its stop moved, on the
Perceptron's opinion alone, decided by scheduling order. Not the vote, not a
weighted blend.

Now the mean across registered members, matching how the ensemble actually trades:
the open decision is the weighted-average vote, and an abstaining member contributes
0 and dilutes exactly as it does there. Members still training read 0, so a
half-trained ensemble reads WEAKER rather than louder - the safe direction for an
exit trigger. Deployed and paused members are included, which is the opposite of the
era barrier's exemption rule and correct for the opposite reason: that one asks who
must be waited for, this asks who has an opinion.

Latent today and staying that way for now by choice - Min_Vote_Close ships Disabled
(101, unreachable on both scales it drives) and TrailingStrategy is off, so live
exits are SL/TP only and the certified hold-to-barrier win rate is what actually
gets traded. Fixed now precisely because the plan is to enable vote exits once the
models are accurate, at which point a scheduling-order exit would be both harmful
and very hard to see.

STILL OPEN, and needs a decision before vote exits go on: the member gate and the
ensemble vote gate both grade hold-to-barrier, so enabling vote exits makes the
certified number stop describing the traded one. Warrior_EA.mq5 currently argues
barrier models may keep vote exits because "their label IS the vote's own horizon" -
that does not hold, since a vote flip at bar 5 of a 64-bar horizon is not the
target-before-stop outcome the gate measured. Either grade the OOS call on the real
exit rule (first of SL / TP / vote-flip / horizon) through the fill engine, or set
HoldToBarrier for ensemble members so the policy cannot drift from the certificate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:15:08 -04:00
AnimateDread
fca610fea0 fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:

  ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
  indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
  Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982

MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.

And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.

1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.

"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.

2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.

A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.

Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.

3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.

ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.

4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.

Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
AnimateDread
be396749fc fix(ensemble+depth): the barrier had no liveness escape, and the depth gate could not report the one state the evidence pointed at
Two charts (USDJPY 50,179 bars / XAUUSD 33,982) sat at era 0 for 38 minutes with
four of their eight members completely silent. Nothing in this commit guesses at
why the sweep fails - the last five guesses were all wrong. It makes the failure
say what it is, and stops one broken member taking its whole chart down with it.

WHAT THE LOG ACTUALLY SAYS, before any of this.

  - The running build IS d9f834d (pulled 14:18, compiled 14:19:01, 0 errors), so
    every depth instrument from 1dda479/7e63a8b/45c9e21 was live.
  - It printed NOTHING. Zero "PRIMING", zero "CAPPED", zero "Per-indicator depth"
    in 27 MB of journal. The instrument built to find the depth shortfall returned
    "not this".
  - On USDJPY at 14:24, CONV-cad8 completed eras 0 AND 1 across all 50,179 bars -
    same chart, same 832-value window, same indicators, byte-identical fingerprint -
    while LSTM-cad8 and HYB-cad8 reported ok=0 failed=50163. So it is not the
    symbol, the history, the bar count or the indicator depth. It is per-member.
  - ok=0 means the NEWEST anchors failed too, and a short indicator cannot do that.

The depth reading in project_silent_block_failures is therefore retired by its own
instrumentation. THE ROOT CAUSE IS STILL UNKNOWN and this commit does not claim one.

1. THE DEPTH GATE'S SILENT PATH WAS THE STATE IT WAS HUNTING.

ServableBars() read `if(servable <= 0 || servable >= want) return want;` - one
branch over three unrelated states, silent in all of them:

  enabled == 0                -> nothing tunable is on. No cap. Healthy.
  enabled > 0, servable == -1 -> a handle answered INVALID.
  enabled > 0, servable ==  0 -> created, never calculated.

BarsCalculated() returns -1 for a dead handle, so a dead MA is indistinguishable
from "no tunable indicators enabled" - and both returned `want` without printing a
character. That is exactly the state a per-member, every-index, depth-independent
failure produces, and it is the single reason a build carrying full depth
instrumentation logged nothing through the whole outage.

TunableBarsCalculated() now also reports HOW MANY indicators it consulted, and the
dead-handle case is reported (latched, with per-handle depths). The RETURN is
deliberately unchanged - what to do about a dead handle is not yet known, and
changing control flow on an unproven cause is how the last four fixes here went
wrong. SettledBars() routes its three pass-through states via ServableBars() so the
report is reachable from the training sweep, which is the only caller that hits it.

2. THE STALL REPORT NAMED A SLOT, NEVER A BLOCK.

"lookback slot 0 REJECTED (window had 24 of 832 values)" plus a guess ("an
indicator warm-up or a history-edge read"). Which guard fired was INFERRED by
counting 4+5+4+4+6+1 = 24 and concluding feature 25 must be the MA. The arithmetic
was right; every conclusion drawn from it was wrong, because a value count names a
POSITION and a position cannot tell cold from capped from invalid from off-the-end.

Every guard that can reject a bar now records itself - m_featureFailBlock - and the
report carries it, the series index, IndicatorDepthReport()'s per-handle depths,
and for each indicator whether the NEWEST bar reads. That last field is the whole
diagnosis in one word: newest-also-EMPTY means the buffer is unreadable everywhere
(cold or dead handle), newest-reads means a genuine history edge. Instrumented:
open, ATR, MA, RSI, MACD, Ichimoku, and all five AD blocks via ADIndicatorCold().

3. THE TOTAL-FAILURE BACKOFF WAS GATED ON THE WRONG QUESTION.

It armed only when m_featureFailTransient was set. Keeping that flag correct across
every guard is a list that has to stay right forever - the same shape of fix the
feature cache abandoned for the same reason - and the gate is pointless anyway: a
sweep where ZERO of 50,163 bars produced a window will produce zero again if it
restarts a millisecond later, transient or not. Doing that at full speed is what
starved six indicator threads on a six-core box. The backoff is now unconditional
on a total failure. The flag keeps its real job, deciding whether a MISS may be
cached, which is a per-bar question and not a scheduling one.

4. THE ERA BARRIER DEADLOCKED, AND SILENCED THE MEMBERS IT FROZE.

EnsembleMinTrainingEra() exempted deployed, stopped and paused members and its
comment concluded "so nothing deadlocks". Those three are all VOLUNTARY. A member
that simply CANNOT finish an era is none of them, so it pinned the minimum at its
own era with no time limit - and the hold branch's only action was
`m_lastEraCompleteTick = GetTickCount()`, which silences the stall watchdog. So on
USDJPY the two members that could not train reported, and the two healthy members
frozen behind them wrote nothing anywhere. The outage was visible only through the
members that were not suffering it.

  - BarrierEraHeartbeat() stamps a clock on real era CHANGE, kept separate from
    m_lastEraCompleteTick precisely because the barrier resets that one. Only a
    member AT the minimum can be a blocker; a member ahead is idle by design and is
    never counted as stuck.
  - After ENSEMBLE_BARRIER_STUCK_MS (12 min) a non-advancing member is dropped from
    the barrier minimum. It keeps training and rejoins the instant it completes an
    era - at which point, being behind, it legitimately becomes the minimum again,
    which is the documented resumed-laggard behaviour.
  - Both transitions say so loudly, and the release states plainly that the
    combined-vote score cannot be computed while the ensemble is desynchronised.
  - A held member now writes a rate-limited journal line naming WHICH members it is
    waiting on, so the blocker is read off one line.

5. THE PANEL FLICKER.

OnTickHandler gates its terse writer on !m_trainRunActive, and a barrier-held member
returns from Train() before ever setting it - so both writers thought they were the
only one updating the label and fought every tick. That is the reported "Getting
ready..." <-> "Waiting at era N for slower ensemble members" oscillation, and it hit
Perceptron but not Convolutional purely because Convolutional had a run active from
a completed era and Perceptron, resumed from disk, never did. Train()'s message is
the specific one, so it wins.

NEXT STEP once this is running: the stall line now ends in "REJECTED BY: ..." and
the per-handle depths. Read it. Do not reason around it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:21:15 -04:00
AnimateDread
d9f834d01d fix(buffers): revert the MA +1 - it asked for a bar that does not exist and stopped every chart
REGRESSION I INTRODUCED IN 1dda479, live for ~15 minutes.

CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory succeeds only when
Bars() >= size. Train() calls ResizeBuffers with barIndex == Bars(), so sizing the MA
buffer to barIndex + 1 asks for one bar more than the symbol has and fails the WHOLE
ResizeBuffers call. The log named it exactly:

  failed to get 50180 bars for USDJPY,PERIOD_H4     (Bars() = 50,179)
  failed to get 33983 bars for XAUUSD,PERIOD_H4     (Bars() = 33,982)

StartLabelCachePrebuild() then bailed on the false return and stayed silent, so the
only symptom was Train() reporting "arming the first label-cache prebuild" forever
with labelCacheBars=0 - the panel's "getting ready".

The premise was wrong, not just the arithmetic. The MA block reads GetData(idx) AND
GetData(idx + 1), and at the OLDEST bar that second read is SUPPOSED to fail - there
is no older bar to difference against. Rejecting that one bar is correct behaviour;
buying it cost the entire history.

Two more things, since the same defect had a second instance and no alarm:

- The Ichimoku pair (closeBars and m_Ichimoku, both barIndex + ichiKijun) is the same
  bug with a far larger constant, latent only because the feature is off. Both are now
  clamped to Bars(). The oldest ichiKijun bars then have no cloud, which that block's
  EMPTY_VALUE guard already handles per-bar - the right outcome.
- The prebuild's bare `return` on a false ResizeBuffers now says so once, naming the
  depth and Bars(). MQL5's own "failed to get N bars" was in the log the entire time,
  from a stack frame nothing connected to the prebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 14:12:47 -04:00
AnimateDread
45c9e211b3 feat(depth): prime -> settle -> sweep, and name which handle is short
"Max bars in chart" is set to Unlimited, so the static-terminal-limit reading in
1dda479 was wrong. Two other candidate causes are falsified too: the price series
is fully downloaded and flat (USDJPY 50,162 -> 50,163 over 80 minutes, i.e. one new
H4 bar), and the handles have been stable since 13:07 with zero windows for the 17
minutes after, so it is not download-in-progress and not handle churn. The MA period
tops out at 200 (ADIndicatorTuner MA_PERIOD_PRESETS) against ~50k bars, so it is not
indicator cost either.

What IS verified stays verified: CopyBuffer past the calculated depth fails outright
rather than short-reading, so the buffer holds nothing and every index reads
EMPTY_VALUE; m_MA is the only CiCustom whose block REJECTS on that (m_ADZigZag
neutral-fills, RSI/MACD/Ichimoku/ATR are built-ins); the wall is therefore feature 25
of every bar, exactly as the "24 of 832" stall lines said. And it is depth-correlated:
16k-bar charts train, 34k/50k get zero windows forever.

So the WHY is still open, and this fix does not depend on it. Per the user's protocol:
the request itself is the primer, so prime at full depth, then poll TunableBarsCalculated()
every 3s and hold the sweep until it stops changing (3 steady probes), then use whatever
it settled at. Bounded at 10 min, and a give-up is logged as a give-up so an abandoned
depth is never mistaken for a settled one. This supersedes 1dda479's clamp on the two
training paths, which snapshotted a value that may still have been climbing; the clamp
remains for the paths that cannot wait (inference/online/rescan/export, see 7e63a8b).

The load-bearing part is what does NOT happen while waiting: no sweep. A 50k-bar feature
scan starves the indicator threads the request just woke, which is how the failure
sustained itself for 40 minutes at a time - discard era, re-sweep, discard, which is the
0->100% oscillation on the panel.

Also adds IndicatorDepthReport(): per-handle BarsCalculated() on the priming, cap and
stall lines. The logs proved WHICH FEATURE died but never WHICH HANDLE was short, so the
cause had to be inferred - and was guessed wrong twice. The next occurrence reads it off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:44:27 -04:00
AnimateDread
7e63a8be01 fix(depth): route EVERY ResizeBuffers call site through one indicator-depth gate
1dda479 clamped the training sweep. It left five other paths asking the indicators
for a depth they cannot serve, and on a live account the quiet ones are worse than
the stall was - a stalled chart is visible, a chart trading on a degraded feature
window is not.

ServableBars(want, context) is now the single gate, and all six go through it:

  training sweep    clamp, floored at TRAIN_MIN_CLAMPED_BARS (below that a small
                    positive BarsCalculated is warm-up, which m_coldSweepTick owns)
  label prebuild    clamp - labels come from price/ADZigZag and would survive a
                    capped MA, but ResizeBuffers sizes EVERY buffer and a failed
                    CopyBuffer leaves m_MA EMPTY for the next reader, so this path
                    could silently re-break the block Train()'s clamp just fixed
  live inference    HOLD. Below `need` the swing block takes its degraded path and
                    inference runs on a different feature distribution than the model
                    was fitted on. This EA sizes real positions off that output, so
                    no signal beats a mismatched one
  online learning   HOLD, same reason and worse - this path WRITES to a live trading
                    model, so a mismatched (features, label) pair is not a wrong arrow,
                    it is a wrong weight update that compounds every bar
  chart rescan      clamp - SIGNAL_RESCAN_LOOKBACK_BARS is 5000 and MT5's smallest
                    "Max bars in chart" is also 5000, so this one is genuinely
                    reachable; uncapped it repaints the window all-Neutral
  research export   clamp before the emptiness test, so a capped symbol exports the
                    depth it has rather than writing a CSV with a dead feature block -
                    an artefact that looks complete and is silently wrong

Both HOLDs are insurance, not expected states: `need` tops out near 1,152 bars
(16 + 750 + 384 + 2) against a 5,000 floor on the terminal setting. They exist so
the failure mode is unreachable rather than merely unlikely.

Not changed: a genuinely SHORT price history still takes the old degraded path at
every site. That is pre-existing behaviour and narrowing it would mute charts that
trade today, so it stays a separate decision rather than a side effect of this fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:33:16 -04:00
AnimateDread
1dda479261 fix(train): clamp the sweep to indicator-servable depth - the scan wall was CopyBuffer, not a cold indicator
Symptom: on a 3-chart run with contention ruled out (SP500 sitting at era 2552),
USDJPY and XAUUSD produced 0 usable windows out of 50,163 and 33,966 - forever,
re-sweeping on every discard, which is the panel oscillating 0->100%.

Bars() is the PRICE series depth. A CUSTOM indicator's is not: MT5 calculates it
in its own context bounded by "Max bars in chart" (TERMINAL_MAXBARS), and
CopyBuffer past that limit does not short-read, it FAILS - so CDoubleBuffer keeps
nothing and EVERY index answers EMPTY_VALUE. ADMovingAverage is the only custom
indicator whose feature block REJECTS on EMPTY_VALUE (ADZigZag, also CiCustom,
neutral-fills; RSI/MACD/Ichimoku/ATR are built-ins served at any depth), so the
sweep died on feature 25 of every bar while the 24 price features under it were
fine. That is exactly the "window had 24 of 832 values" the stall report named.

Perfectly depth-correlated, measured 2026-08-17:
  SP500  16,234 bars -> era 2552     XAUUSD 33,982 -> 0 windows
  XTIUSD 16,611 bars -> era   71     USDJPY 50,179 -> 0 windows

This RETIRES the 2026-08-17 cold-indicator reading of the same stall. f0cf659 was
right that the rejection must be transient and that the dead backoff had to arm -
the branch did change to 'cold-indicator backoff' - but waiting cannot fix a depth
the terminal will never grant. So Train() now clamps to TunableBarsCalculated()
(which existed and was only ever used for a tuner printout) and trains on the
history that IS available, naming TERMINAL_MAXBARS in the log so the cause is
readable next time. m_coldSweepTick still owns the genuinely transient case: that
reads back as -1, not a positive short count. Recomputed per era, so the clamp
lifts by itself if the setting is raised.

Also fixes a real off-by-one it was hiding: the MA block reads GetData(idx) AND
GetData(idx + 1) for its bar-over-bar change, but ResizeBuffers sized m_MA to
barIndex exactly - so the deepest bar of every sweep read one past the end and was
rejected as cold. Same shape as the +ichiKijun the Ichimoku/close pair already has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:27:14 -04:00
AnimateDread
0c38bfc9ab fix(pooledgate): _Period, not Period() - the bare call resolves to CExpertBase's setter
Three compile errors, all the same cause. Inside a CExpertBase subclass a bare
Period() no longer reaches the builtin ENUM_TIMEFRAMES Period(); MQL5's method-
hiding rules resolve it to the inherited bool CExpertBase::Period(ENUM_TIMEFRAMES)
setter, which takes an argument - hence 'wrong parameters count, 0 passed, but 1
requires'.

Switched to the _Period predefined variable, which is what the rest of this
codebase already uses (100 occurrences; ::Period() appears nowhere). AutoTune.mqh
line 82 builds its own per-symbol/timeframe filename exactly this way, so the
pool file naming now matches the convention it should have followed from the
start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:57:43 -04:00
AnimateDread
1cf4c57d57 fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.

CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.

And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.

Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.

THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.

SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
AnimateDread
f0cf659945 fix(features): a cold indicator is TRANSIENT, not a permanent miss - and back off instead of re-sweeping
Six fresh instances on USDJPY and XAUUSD swept 33,965-50,162 bars and produced
ZERO usable feature windows, repeatedly, for 40 minutes and 239 stall reports,
without ever completing era 0. The four instances already warmed up before those
charts were attached trained normally throughout.

THE STALL REPORT NAMED THE SPOT EXACTLY: 'lookback slot 0 REJECTED the bar
(window had 24 of 832 values)', and 24 is the core block to the value - 4 price +
5 swing + 4 range + 4 volume + 6 time + 1 ATR. So feature 25 was the wall, and
feature 25 is the first value of the MA block. The same 24 appeared on XAUUSD
against a 816-value window (51 features/bar vs 52), which is what ruled out any
symbol-specific data gap: the wall sits at a fixed feature index, not a date.

ADMovingAverage is a CUSTOM indicator, so MT5 fills its buffer asynchronously and
returns EMPTY_VALUE for EVERY index until it has calculated - not just the
warm-up tail. That guard did not set m_featureFailTransient, so every bar of the
sweep was cached as a PERMANENT miss. This is precisely the failure the ATR guard
twenty lines above it was fixed for on 2026-08-10; the fix was never propagated
to the indicator blocks that follow. RSI, MACD and Ichimoku had the same defect
and are fixed too. (The Donchian high/low guard is a break into a
degraded-but-usable path, not a rejection, and is deliberately left alone.)

IT ALSO SELF-SUSTAINED, which is why it never recovered. The ok=0 self-heal drops
the feature cache and re-sweeps immediately, so each stuck instance spent every
millisecond re-reading 30-50k bars - six of them at once, on a six-core box,
competing for CPU with the very indicator calculation they were all waiting on.
The recovery was preventing the recovery. A transient total failure now re-arms
m_warmupPassesRemaining, yielding the CPU for a few separately-scheduled Train()
calls - the same mechanism a fresh model already uses to let history sync finish,
pointed at indicator warm-up instead.

Verified in the terminal journal first: indicators load and unload in matched
counts and there is no OOM, so this is NOT the 33f106d handle leak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:47:25 -04:00
AnimateDread
d87f7d88ff feat(gate): cross-instrument pooled certification
The deploy bottleneck is CERTIFICATION, not training. A 4,738-bar OOS window at
L=75.6 holds ~63 independent observations; certifying a 3pp edge at 2 sigma needs
~1,036. More bars of the same symbol barely help - they overlap. Other symbols do
not.

WHAT POOLS. Not win rates: symbols have different derived geometries, different
break-evens and different drifts, so averaging raw rates across them is
meaningless. What pools is each symbol's EXCESS OVER ITS OWN CHANCE RATE,
combined by inverse-variance weighting (fixed-effects meta-analysis). Each symbol
keeps its own model, geometry and chance rate; only the evidence is combined.

THE CORRELATION PROBLEM, bracketed rather than assumed away. SP500 and NAS100 are
~0.9 correlated and pooling them as independent inflates the evidence. Nothing
here can measure that without sharing return series, so instead of guessing a
correction the gate reports both ends:

  SE_INDEP = sqrt(1/SUM(1/var_i))     all members independent
  SE_CORR  = SUM(w_i * sqrt(var_i))   all members perfectly correlated

The truth is always between. THE GATE USES SE_CORR, so a pass cannot be an
artifact of correlated instruments - that bound already assumes the worst. The
ratio is logged as the diversification credit the gate declines to claim, so the
cost of that conservatism is visible instead of hidden.

SCOPE, deliberately limited: the pooled result is REPORTED, never folded into
tradeableOK. The local gate certifies the model that actually trades this symbol;
the pool answers the different question of whether the strategy has an edge at
all. Letting a cross-symbol result license a local deploy would ship a model that
never cleared its own bar - so it cannot.

Mechanics: one file per instrument (no concurrent-write path to get wrong), every
FileOpen carrying FILE_SHARE_READ|FILE_SHARE_WRITE, records skipped rather than
reinterpreted on a version mismatch, 12h staleness cutoff so a stopped chart
cannot vote, and pooling refused below 3 instruments. Poolability requires
matching timeframe and ratio; differing SYMBOL is the entire point. Publishing is
unconditional - a pool that only hears from winners is a selection effect, not a
meta-analysis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:45:37 -04:00
AnimateDread
2b5d0f8355 feat(measurement): fix zero-skill denominator, publish the deploy bar, measure lifespan per rung, add a MEASURE scale objective
The last run could not have demonstrated an edge either way, and nothing in the
log said so. Four changes so it does.

1. THE ZERO-SKILL LINE DIVIDED BY THE WRONG DENOMINATOR. m_oosWinLongTotal resets
   every era; m_oosSamples only resets on a full model reset. So 'always-long %'
   decayed as ~1/era: a run whose true rate is 37% printed 1.2% at era 33 and
   0.0% at era 2219. This is the SAME bug already found and fixed for
   logBuyPredPct thirty lines above ('era-15 Buy:2% that was really ~30%'), left
   in the one line whose whole job is to be the reference every other number is
   read against. Correct at era 1, wrong everywhere after - including the '62%
   zero-skill' figure in the 2026-08-16 notes. Now per-era, and always-short is
   finally readable.

2. THE DEPLOY GATE STATES ITS OWN BAR. 'edge -1pp' era after era cannot separate
   'short by a hair' from 'short by an amount no strategy could cover'. The era
   line now prints the required win rate, the SE, the effective n and the
   lifespan it was deflated by; above 100% it says UNREACHABLE. At 4,738 OOS bars
   and L=75.6 there are ~63 independent observations, putting the bar near 66% at
   typical coverage.

3. LIFESPAN MEASURED PER RUNG. The first-passage cache already stores touch ages
   at every ladder level, so each candidate geometry's resolution time is
   readable without training on it - L-vs-width becomes a measurement across the
   whole ladder in ONE run rather than a second chart. Each rung reports L,
   n_eff, min provable edge and min provable EV.

4. SCALE OBJECTIVE IS PHASE-AWARE, defaulting to MEASURE. Width and detectability
   are opposed: labels overlap by L, L grows like m*k = width^2 at fixed ratio,
   so min provable EV ~ width^2 while the cost saving from width is only linear.
   Doubling width quadruples the smallest EV you can prove. DEPLOY (widest that
   clears reachability) is right once an edge is known; MEASURE (narrowest that
   keeps round-trip spread under BARRIER_MAX_COST_FRACTION_PCT) is right while it
   still has to be shown. The direction does not depend on the exponent, and
   item 3 makes the exponent checkable.

Fixed in review: m_lastRungLifespan is cleared on every LadderWinShare entry or a
rejected rung reports the previous rung's lifespan as its own; per-rung
detectability is labelled IS-sample based (the deriver may not see the holdout),
so absolute figures are optimistic while the ranking is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:37:05 -04:00
AnimateDread
d1ac18ebdb fix(labels): correct EffectiveSampleSize clamp order, share the horizon ladder, retract a false justification
Self-review of 1540ba8 against the FULL 6,930-era log rather than the first
three minutes of it. Three corrections.

1. EffectiveSampleSize() clamped in the wrong order. MathMax(2, MathMin(eff,
   rawN)) returns 2 when rawN is 1 - an effective sample LARGER than the raw
   one, shrinking the SE in exactly the direction the function exists to
   prevent. Floor first, cap at rawN last.

2. The horizon cap rejected on the CEILING only, and said so as though that
   made the label untruncated. It does not: the horizon ladder also snaps DOWN,
   so a pair needing 317 bars is granted 256 and is silently truncated without
   ever being flagged CLAMPED. Added SnapHorizonToLadder() / GrantedHorizonBars()
   and the scale ladder now reports "needs N gets M" per rung. Rejection stays on
   the ceiling alone - matching ReportGeometryExpectancyScan's '!' exactly, which
   was the point - because rejecting on the snap-down would select rungs for
   landing just above a ladder point rather than for anything about the market.
   ComputeBarrierHorizonBars' private copy of the ladder is gone; there is now
   one copy, which is the whole reason RequiredHorizonBars was factored out.

3. RETRACTED THE JUSTIFICATION IN 1540ba8's COMMENTS. That commit claimed the
   overlap correction was needed because the operating point's null-of-the-
   maximum gate fired on 47/73 Perceptron eras (64%) where a family-wise test
   should fire on ~5%. Those 73 fits were the first three minutes of a
   six-and-a-half-hour run. Over the full run:

     PAI  47/3214 = 1.5%    HYB  30/1200 = 2.5%
     CONV  4/63   = 6.3%    LSTM 75/915  = 8.2%

   All at or below the null. The gate from 7414570 is working as designed and
   PAI's 47 clears were a cold-start transient never repeated in 3,141 later
   fits; its threshold over the run's second half has sd 0.01. The overlap
   correction is still right - sqrt(p(1-p)/n) on overlapping labels is the wrong
   formula - but it fixes no observed failure, and it costs nothing today
   because no model is near the deploy line.

WHAT THE FULL RUN DOES CONFIRM, unchanged: the geometry ran away exactly as
described (2.00/6.00 h128 -> 3.49/6.99 h256 -> 4.86/9.71 h384, three passes,
stopping at q90 because the quantile ladder ended), the label stayed long-skewed
at Buy 42.9% / Sell 22.6%, and no checkpoint on any of the four models ever
cleared the deployability floor. Pooled declustered win rates: PAI 31.70%,
HYB 31.02%, LSTM 32.69%, CONV 31.57% - every one 4-6pp below the 37% always-long
chance rate and 1-2.7pp below the 33.7% cost-adjusted break-even.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 07:30:19 -04:00
AnimateDread
1540ba8e64 fix(labels): overlapping-label sample correction + horizon cap on the scale ladder
Three defects, all surfaced by the 2026-08-17 SP500 H4 run that shipped
stop 4.86 / target 9.71 (width 14.57*ATR, horizon 384).

1. EVERY STANDARD ERROR ASSUMED INDEPENDENT SAMPLES. Triple-barrier labels
   started one per bar overlap by the label's lifespan, so n calls are worth
   ~n/L independent observations (Lopez de Prado, AFML ch. 4 - sample
   uniqueness). All three sqrt(p(1-p)/n) sites divided by the RAW count.

   The tell: the operating point's null-of-the-maximum gate is family-wise and
   should fire on ~5% of eras under the null. Measured fire rates - PAI 47/73
   (64%), ConvLSTM 9/24, LSTM 8/21 (38%), CONV 4/62 (6%). CONV, the only model
   whose margin distribution admits few bins, sat on the null; the rest cleared
   a bar that was too low by ~sqrt(L). PAI's deployed threshold consequently
   alternated between the ENDS of its own range era to era (0.10 -> 0.88 ->
   0.86 -> 0.66; coverage 16% <-> 73%).

   TripleBarrierLabel now records when each label became KNOWABLE - the first
   winning touch, or both stops, or the timeout - and the prebuild accumulates
   the mean. EffectiveSampleSize() feeds the operating point, the member deploy
   gate and the ensemble vote gate. Conservative by construction (n/L is an
   upper bound on the damage); gates get harder, never easier.

2. THE SCALE LADDER RAN AWAY, again. Horizon scales as swingMedian*sl*tp, and
   since 4d8cb08 reachability is measured OVER that horizon - so a wider rung
   buys itself the time that makes it look reachable. Same target -> horizon ->
   reach -> target loop the excursion window is kept short to avoid; fixing the
   window confusion reopened it through the other door. It walked 128 -> 256 ->
   384 bars and stopped at q90, the widest rung there is, with every rung
   reading 39-48% against a 20% floor. A floor nothing fails selects nothing.

   Rungs whose required horizon exceeds BARRIER_HORIZON_MAX are now rejected -
   the same rule ReportGeometryExpectancyScan already applied. It was printing
   the shipped pair as CLAMPED and disqualified ('h384!') two lines under the
   deriver that chose it: two subsystems, one geometry, opposite verdicts.

3. THE RUNG SNAP DESTROYED THE RATIO IT WAS COMPARING. Both legs snapped
   independently to the coarse first-passage grid, re-rating each candidate:
   q90 4.86/9.71 -> 5.00/10.00 (2.00), q85 4.07/8.14 -> 5.00/10.00 (IDENTICAL
   measurement), q75 3.07/6.13 -> 4.00/6.50 (1.63 - a nearer target). So the
   ladder compared win shares taken at ratios from 1.63 to 2.17 and read the
   differences as scale. It is why the reach column came out non-monotone in
   width (q75 48.5% above q90 42.9%). The stop now snaps to its nearest rung in
   log space and the target follows the ratio off it; the pair actually measured
   is returned and logged, so a collision reads as a collision.

Also: LadderWinShare guarded against the conditional (fractal) geometry path,
which fills n from m_fracLegCount while leaving idxList empty - a latent
out-of-bounds on a currently-dead path.

New log lines: mean label lifespan and effective n on the label-cache line, the
required-vs-available horizon per rung, and the grid pair the reconciliation
actually measured (its tolerance now scales with the grid skew instead of a flat
5pp).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 01:12:05 -04:00
AnimateDread
4d8cb08501 fix(geometry): the reachability floor measured the WRONG WINDOW - my bug from bc57aca, and it cost real width
RECONCILED: the derivation reported "target reached on 17.7% of bars" while the
label cache reported Buy on 35.9%. Nothing was broken. They measure different
windows, and both are correct:

  EXCURSION window   ~12 bars (the SWING MEDIAN) - what m_excUpCache accumulates
                     over. Deliberately short: sizing a barrier off travel
                     measured over a horizon that itself scales with the barrier
                     is circular, and it ran away to 14-31*ATR on EURUSD/USDCAD
                     in 2026-08-07. That guard is correct and stays.
  BARRIER horizon    64 bars - what the LABEL walk and the first-passage ladder
                     run over, and how long the EA actually holds the trade.

So `up >= target` is a 12-bar question and `label == Buy` is a 64-bar one, and
the second can freely exceed the first. TripleBarrierLabel gates the excursion
accumulation on `idx - t <= excWindow` while the barrier walk and the ladder run
the full horizon - the split is explicit and intentional.

THE BUG IS MINE. bc57aca's scale ladder tested reachability with `up[i] >= tp`,
i.e. it asked the 12-bar question about a 64-bar trade. That understates
reachability by ~2x, which is why EVERY wide rung was rejected and the geometry
fell back to the tightest rung at 1.61/3.21. The data supported considerably
wider; the test was just asking the wrong question.

FIX: LadderWinShare() reads the answer off the first-passage ladder - target
touched strictly before the stop, over the full horizon, tie to the stop. That
is the identical question the label walk asks, so the ladder share and the Buy
rate should now agree to within rung discretisation. Both legs snap to the
SMALLEST rung at or above the requested multiple (harder target, harder stop) so
the floor stays conservative.

Expect the scale ladder to select a WIDER rung on the next relabel. On this
data the excursion test read 17.7% at q50 where the true full-horizon share is
35.9%, so rungs that scored 8.1% and 2.8% were likely well above the floor.

ALSO:

- Window reconciliation now PRINTED every derivation: excursion travel share,
  ladder win share, and the label cache's Buy share side by side, with the
  ladder-vs-label gap flagged if it exceeds rung discretisation. Those two must
  agree; if they ever stop agreeing, one of them is wrong and the line says so.
- Renamed tpReach/slReach -> tpTravel/slTravel and relabelled the log line. They
  describe the EXCURSION window and are near-tautological there (a q50 stop is
  exceeded by ~50% of bars); calling them "reached within the horizon" is what
  made the two quantities look like one.
- BARRIER_MIN_TP_REACH_PCT is now BARRIER_MIN_REACH_FRACTION_OF_BE (0.60) x
  break-even instead of a hardcoded 20.0. Break-even for 1:RR is 100/(1+RR), so
  the absolute floor silently tightened as RR rose - 0.60x at RR=2 but 0.80x at
  RR=3, penalising the user for asking for a bigger target. Evaluates to exactly
  20.0% at the shipped RR=2, so this is a no-op today and correct if the knob
  moves.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:51:08 -04:00
AnimateDread
bc57aca15d fix(geometry): the target was small BY CONSTRUCTION - ratio is now policy, scale is measured, ladder ceiling removed
The derivation read the stop from q75 of ADVERSE travel and the target from q50
of FAVOURABLE travel. Over one horizon those distributions are broadly the same
shape, so q75 > q50 MECHANICALLY - the target came out smaller than the stop no
matter what the market did. SP500 H4 shipped stop 3.07 / target 1.70: a 0.55:1
payoff needing 64.3%. That was never a measurement, it was two mismatched
constants.

The reachability line printed beside it - "target on 50.0% of bars, stop on
25.0%" - is exactly 1-q50 and 1-q75. Tautological. It cannot disconfirm
anything, and it read as validation.

WIDTH AND RATIO ARE INDEPENDENT AND ONLY ONE PAYS. EV = edge x width;
ratio is EV-neutral (a driftless walk reaches +m before -k with probability
k/(k+m), which IS break-even). Width is what buys cost efficiency: the spread
is a fixed 0.047*ATR here, so the shipped 4.77*ATR width paid it 21 times per
unit of travel. So:

  RATIO  = policy. BARRIER_TARGET_RR = 2.0 (user's 1:2). Break-even 33.3%.
  SCALE  = measured. The stop quantile is chosen from a ladder, WIDEST FIRST,
           taking the first rung whose implied 2x target is still reached often
           enough to be a trainable class.

That last clause is the difference from the min-reward:risk raise removed in
2026-08-09, which forced target = 2 x stop with NO reachability test, landed on
6.66*ATR reachable on 3.3% of bars, and trained the model to predict something
that essentially never happened. Same ratio; the scale now retreats until the
data says the target is attainable. Every rung is logged.

LADDER CEILING REMOVED. BARRIER_LADDER stopped at 5.00 and the expectancy scan's
"best resolvable pair on width alone" came back as stop 5.05 / target 4.95 - it
pinned to the top rung. A recommendation landing exactly on the edge of its own
search space is a boundary, not a finding: it cannot tell "5 ATR is optimal"
from "5 ATR is all we allowed". Extended to 20*ATR (8 -> 14 rungs). Nothing else
needs editing - every consumer is parameterised by BARRIER_LADDER_COUNT - and
the horizon constraints (decided >= 60%, reachability floor) now bind instead of
a constant.

THE SCAN COULD NOT SEE THE SHIPPED GEOMETRY. ReportBarrierGeometryScan looked
the configured pair up in its integer grid, and DeriveBarrierGeometry produces
CONTINUOUS multiples (3.07/1.70) that can never equal a grid point - so
cfgExcess stayed at its -1.0 sentinel and the report printed "configured 3:2
scores -1.00000", which reads as a catastrophic score and actually means "never
evaluated". Worse, the grid skipped target<stop entirely because it "inverts the
trade's whole premise" - while the derivation was shipping exactly that. The
incumbent is now always scored as a peer (never crowned; it is already in force
and is not an enum pairing the scan could adopt).

BREAK-EVEN NOW INCLUDES THE SPREAD. Every report quoted the frictionless
SL/(SL+TP). On SP500 H4 that read 64.3% while the MEASURED zero-skill rate was
62.1% - a 2.2pp gap that IS the cost, and that made every model look 2.2pp
better than it was. CostAdjustedBreakEvenPct() prices a win at (TP - spread) and
a loss at (SL + spread), matching the expectancy scan's convention exactly so
the two reports cannot disagree.

It also feeds FitDirConfThreshold, which is the correctness half: the operating
point subtracts break-even from precision, so the frictionless figure made every
candidate threshold look better by the width of the spread - 2.2pp against a
measured edge of 2.3pp, i.e. very nearly all of it.

Era line now carries both: "break-even 64.3% frictionless, 66.6% AFTER SPREAD".

Forces a full relabel and retrain. Requested.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:34:32 -04:00
AnimateDread
d30420e3f2 fix(batchnorm): bound the normalized value - a constant input feature was amplified 1e4x and pinned PAI's head to its rails
BN_MIN_STD = 1e-4 caps the per-unit gain at 1/1e-4 = 1e4, and the comment above
it states that as though it were a safety property. It is not. A unit whose
running variance is ~0 is a CONSTANT feature carrying no information, and
dividing its rounding noise by 1e-4 hands the next layer an activation of
several hundred. BN's contract is "output has ~unit variance"; a unit that
cannot supply that must contribute nothing, not the largest signal in the layer.

MEASURED, 2026-08-17 SP500 H4, four topologies on identical separate charts:

  model   spread   Neutral CHOSE   Neutral TIED   rail
  CONV     0.386        0.68%          0.10%      0.48%
  LSTM     0.392        0.63%          0.00%      0.00%
  HYB      0.376        1.79%          0.00%      0.01%
  PAI      0.192        0.09%         80.63%     99.99%

bn1's cached nx normed 1.38e4 over 800 units. PAI's SIGMOID head was on its
rails on 99.99% of bars, with Buy and Sell landing on the SAME rail so they
compared exactly equal, and ApplyClassificationSoftmax()'s strict-majority rule
reported that tie as Neutral on ~80% of bars.

So the long-running "PAI is heavily biased toward Neutral" was never a
class-prior problem: the net CHOSE Neutral on 0.09% of bars. It was float
equality on a saturated head. The 331ab29 counters answered it on their first
run.

PAI-only because it is the one topology whose FIRST batch norm sits on the raw
800-dim input vector - CONV/LSTM/CONVLSTM all have a conv or LSTM stage in
front, so their first BN sees a learned representation with no degenerate
units. That asymmetry was already on file as a suspicion; this is the mechanism.

FIX, mirrored in both backends (host NeuronBatchNorm.mqh and device Network.cl):

  forward   nx = clamp(delta/sd, -BN_MAX_NX, +BN_MAX_NX), BN_MAX_NX = 8
  backward  if the forward bound this unit, the output stopped depending on the
            input, so d(nx)/dx = 0 and NO gradient passes

The backward half is not optional. g is divided by the same sd the forward
multiplies by, so a degenerate unit gets its GRADIENT amplified 1e4x too - the
"receives gradients divided by sqrt(var) ~ 500" pathology already noted in
Network.cl's Adam kernel. Bounding only the forward would move the explosion
downstream.

8 sigma is inert on anything healthy (|nx| > 8 is a ~1e-15 event under
normality); it binds only on degenerate units, which is the entire point. Same
clamp-to-range idiom the activation derivatives beside it already use.
SelfCheckBnForward/SelfCheckBnHiddenGrad already prove host against kernel, and
BN_OPT_NX was already consumed in the backward for the gamma gradient, so the
new read adds no lifetime assumption.

Expect PAI to change behaviour and CONV/LSTM/CONVLSTM not to (their rail rate is
~0%, so the clamp never binds). No .nnw format or fingerprint change.

ALSO: print the zero-skill reference on the era line. m_oosWinLongTotal and
m_oosWinShortTotal have been accumulated for a long time and NEVER printed,
which is why three separate topologies all sitting at 62% read as a mysterious
coincidence rather than the obvious base rate. It is not a coincidence: with the
target (1.70 ATR) nearer than the stop (3.07 ATR), BOTH sides win on 24.5% of
bars, so winLong+winShort covers ~124% of them and a no-edge caller collects
62.1% whichever way it calls - against a 64.3% break-even. Derived from this
run's own label counts: (11329 - 2776 + 2*2776) / (2*11350) = 62.14%.

Every win rate on that line must be read against this, not against 50%.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:21:51 -04:00
AnimateDread
331ab29c56 feat(diagnostics): split a reported "Neutral" into CHOSE vs TIED - they need opposite fixes
ApplyClassificationSoftmax() requires a STRICT majority over both rivals and
sends every tie, 2-way or 3-way, to Neutral. So "OOS recall Neutral:100%" is
two completely different events sharing one label:

  CHOSE - the net genuinely ranks Neutral highest. A class-prior/label problem.
  TIED  - the top two are EXACTLY equal, so the net expressed no preference and
          the tie-break reported Neutral. A SATURATION problem: the head is
          SIGMOID, and a saturated sigmoid returns exactly 0.0f or 1.0f in the
          DLL's float32, so two classes pinned to the same rail compare equal
          and the bar is silently discarded.

Nothing in the logs could tell them apart, and the fixes point opposite ways.
Eras 1-25 of the 2026-08-17 solo PAI run read "Neutral 100%" at spread avg 0.99
- fully saturated - and broke out at era 27 as the spread fell to 0.75. That is
consistent with EITHER story. The user reports the Neutral phase on most runs,
so it is worth four longs to stop guessing.

Four per-era counters on the pass 3 OOS walk, reported as:

  | Neutral CHOSE 12.4% / TIED 38.1% (of which B=S 1204) | rail 61.2%

  m_oosNeutralStrict - Neutral strictly highest
  m_oosNeutralTie    - no strict winner; the tie-break produced Neutral
  m_oosTieBuySell    - the costly subset: Buy and Sell tied AT the top, i.e. a
                       DIRECTIONAL reading thrown away by float equality
  m_oosRailBars      - any raw output sitting on a sigmoid asymptote, the
                       saturation that makes exact ties possible at all

Read on the RAW logits, before ApplyClassificationSoftmax() overwrites TempData
in place. Legitimate because softmax is strictly monotone: it cannot change the
ordering and cannot break a tie either, so the raw reading and the decision
always agree. Placed alongside the existing min/max/spread capture so all the
output diagnostics describe the same values.

Measurement only - no decision path reads these.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:06:54 -04:00
AnimateDread
7414570d9d fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.

1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
   getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
   mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
   of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
   block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
   of their own denominator, a 1022x inflation. NX is also near-constant between
   era-end reports (same last forward pass), which pins the numerator down too,
   so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
   frozen first layer. It was the ruler that was broken. The ratio now covers
   trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
   excluded. NX is reported separately because it is a health signal in its own
   right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
   ~1.0, which is what a near-zero running variance in the denominator looks like.

   NO historical dW/W reading on a bn* layer is admissible evidence that a layer
   did or did not train. That includes every such claim in this repo's notes.

2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
   over 50 bins. Measured across 98 consecutive fits:

     correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
     win rate stdev across fits                    = 1.32pp
     binomial SE of that win rate at ~1430 calls   = 1.25pp

   The correlation is zero - the margin does not rank trades - and the era-to-era
   spread IS its own sampling error to within 0.07pp. So the objective was
   coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
   whichever bin drew the luckiest sample. The threshold teleported
   0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
   the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
   entire reported instability.

   The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
   selective bin still clearing the coverage floor, chosen from the margin
   distribution alone and never from a win rate - by more than a best-of-N
   maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
   maximum correction the deploy gate already applies to model selection.

   A plain one-standard-error band was tried first and is NOT sufficient: its
   edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
   edge is itself +/-50%, so the admitted set would still wander by half its own
   width every era. The fallback has to be independent of the noisy quantity.

   Simulated on the observed numbers: falls back every era at the current 2.3pp
   edge (stable), adopts the argmax once a real edge reaches ~5pp.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00
AnimateDread
208da4cbaa fix: drop the ranking slice for the calibration band; un-collapse the tiers
NOT COMPILED - user compiles.

(1) THE RANKING SLICE IS GONE. It reserved 20% of the OOS window so the
pattern-DB backfill would read bars the deployed checkpoint was not SELECTED on.
That objection stands; carving a new region to answer it did not. The calibration
band already has every property the slice was buying:

  never trained on | never graded by pass 3 (which walks [0, oosCutoff) and so
  never reaches it) | never seen by the deploy gate | purged by a full label
  horizon on BOTH sides | and larger besides - 1,684 bars vs the ~970 carved

So the backfill now walks [calibLo, calibHi) and pass 3 goes back to grading the
entire OOS window, exactly as before any of this. The gate gets its full sample
back (~10% of a sigma), the split loses a region, and the failure mode found an
hour ago - a reserved region silently blanking ~10 months of chart arrows,
because arrows are only drawn on bars pass 3 grades - becomes impossible.

One impurity, stated in the completion log rather than hidden:
m_dirConfThreshold is FITTED on that band and the walk applies it to decide which
bars fired, so coverage there is mildly optimistic. One scalar under a coverage
floor, against checkpoint selection over hundreds of eras.

This backfill IS the deploy-time warm-up: it runs right after FinalizeTrainRun()
restores the deployed weights, so it scores with exactly what is about to trade.

(2) EVERY CALL WAS TIER 0, AND IT WAS ARITHMETIC. ConfidenceTier() quartiles
[floorConf, 1] where floorConf = 1/3 - the lowest magnitude a 3-way softmax
winner can hold. But it was fed CalibratedConfidenceMagnitude(), which multiplies
by m_confidenceCalScale, clamped to [0.3, 1.5]. That lower clamp is BELOW 1/3.
Whenever calibration bottoms out, t goes negative and MathMax(0, ...) pins every
call to tier 0.

Which is what the live run does. m_confidenceCalScale is EMA'd toward
empiricalAccuracy / avgClaimedConfidence; with the model over-calling Neutral,
3-class agreement sits near 10% against a claimed confidence near 0.9, so the
ratio is ~0.11 and clamps to 0.3 every era. Logged:

  tier prec T0:72%(828) T1:n/a(0) T2:n/a(0) T3:n/a(0)

828 calls, one bucket - the four tier weights and the entire per-tier pattern-DB
ranking reduced to a single number. The backfill was feeding a mechanism that
structurally could not rank.

Tiering now reads the RAW head magnitude, which genuinely lives on the
[1/3, 1] range these bounds were written for. Calibration keeps its real jobs -
AIConfidence() for MM sizing and SignedAIConfidence() for the vote are unchanged.

STILL OPEN, deliberately not touched here: the calibration TARGET itself.
empiricalAccuracy is 3-class agreement, which is the wrong quantity to scale a
DIRECTIONAL confidence against - it counts a Neutral class that is 0.19% of
labels. The honest target is the win rate on the calls the confidence describes
(directional precision), with the claimed-confidence average taken over those
same called bars. That needs a new accumulator and it interacts with the Neutral
over-calling being fixed elsewhere, so it wants one clean run first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:06:11 -04:00
AnimateDread
75d23e9b82 fix(gate): move the ranking slice to the OLD end - it walled off the recent chart
NOT COMPILED - user compiles.

User: "there is quite some trading going on, but absolutely nothing on the recent
area of the chart, like there is a hard wall starting around november 2025."

That wall is 7caf2f6's ranking slice, and it was placed at the wrong end. Chart
arrows are only ever drawn on bars pass 3 GRADES, and the slice reserved the
NEWEST 20% of the OOS window plus a label-horizon purge. At the live sizing -
~4,860 OOS bars, 128-bar horizon - that is ~1,100 H4 bars withheld from grading,
about ten months back from today, exactly where the wall appears.

The invisible cost was worse than the visible one: it handed the deploy gate the
OLDEST 80% of the OOS window and withheld the most recent regime from the single
decision that has to generalise forward.

Both fixed by putting the reserve at the oldest end instead:

  [0, oosScoreHi)        OOS - graded by pass 3   (NEWEST, arrows restored)
  [oosScoreHi, rankLo)   purge - one label horizon
  [rankLo, oosCutoff)    RANKING - backfill only, graded by nobody
  [oosCutoff, calibLo)   purge
  [calibLo, calibHi)     CALIBRATION
  ...                    IS

Of the three consumers competing for those bars, recency is worth least to the
ranking: it is an ORDERING of confidence tiers, far less regime-sensitive than an
absolute win rate, while the gate's power and the operator's read of the chart
both want the newest data. The slice keeps every property that made it worth
carving - never graded, never selected on, never seen by the gate, purged on both
sides - so the backfilled rows are still honestly out-of-sample.

RankSliceHiIndex is replaced by RankSliceLoIndex + OosScoreHiIndex; pass 3 now
excludes the slice at the TOP of its walk and descends to 2 as it always did.
The backfill walks [RankSliceLoIndex, oosCutoff) via a new m_dbBackfillStopIndex,
clamped at both ends so a degenerate slice yields an empty walk rather than one
that wanders into graded bars. Verified no reference to the old helper survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:58:55 -04:00
AnimateDread
1b5a412946 fix(imbalance): the class-imbalance correction was subsidising the abstain class
NOT COMPILED - user compiles.

Root cause of the Neutral collapse. Logit adjustment (Menon et al. 2020) makes a
classifier Bayes-optimal for BALANCED error by subsidising rare classes. It was
wired here when Neutral was the DOMINANT class - the "big move up / big move down
/ nothing much" era, where the correction pulled the model off the majority.

The triple-barrier relabel (b4a704d) inverted the distribution. The barriers are
now the EA's own SL/TP, so ~89% of bars RESOLVE and only timeouts are Neutral.
Measured on SP500 H4, from the EA's own log:

  measured priors Buy 48.26%  Sell 41.13%  Neutral 10.61%
  log-prior spread 1.52 | tau 1.00 CAPPED to 0.79

Neutral became the RAREST class, so the correction started subsidising it - by
tau*(log pB - log pN) = 1.20 logits. With no directional edge to overcome that
(direction is closed at best-of-999, p=1.0000), the model took the free lunch:

  OOS recall Buy:1% Sell:0% Neutral:100%
  OOS raw out spread avg 0.9993        (softmax saturated, near one-hot)
  dW/W bn1 0.000%  bn3 2.0%  bn5 6.5%  (input weight block frozen; head twitching)

The anti-collapse mechanism was the collapse. The recall gate needs >=40% on all
three classes, so nothing could ever deploy and the plateau ladder burned eras.
Present in both runs today (b6b5 froze bn1 by era ~719, 17ae by ~169), so it
predates this week's work.

FIX: the correction now spans the DECIDABLE classes only, Buy against Sell,
centred on their midpoint, with Neutral pinned at offset 0. Neutral is the
ABSTAIN outcome and abstention already has a better owner - m_dirConfThreshold,
refitted every era on the held-out calibration band against a coverage floor and
the measured break-even. Subsidising the abstain class does that job twice and
spends the whole correction suppressing the only decisions that can pay.

What still gets corrected is real: a trending symbol resolves more long barriers
than short, and uncorrected the model inherits that as a standing directional
bias. Here it is log(0.4826)-log(0.4113) = 0.16, so the offsets are tiny - the
correct answer, not a broken one. The two traded classes were already balanced;
the old spread of 1.52 only ever described how rare a timeout is.

Everything is derived from the measured distribution, as requested - offsets from
the priors, cap from the resulting spread. tau itself is deliberately NOT fitted:
tuning it against the same data that selects the checkpoint would add another
search dimension to a project that has been burned by exactly that. tau=1 is the
theory value and the cap (now ~9.5x looser at spread 0.16) will rarely bind.

Log line now reports both spreads and, when the abstain class is the rarest, says
how much the old form would have boosted it. Fingerprint |LA:<tau> -> |LA:<tau>:BS
so models trained under the all-three form re-key instead of resuming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:43:30 -04:00
AnimateDread
1eeed3ac06 revert(ui): restore the unconditional era-end arrow repaint
b197999 gated the repaint on "this era beat the best checkpoint". That was not
what was asked for and it changes policy rather than fixing a bug, so it is
reverted - behaviour is now byte-identical to before b197999. Only a comment
recording what was verified remains.

What the check found: there is NO repaint or erase at the start of an era. With
NMS on, passes 1, 2.5 and 3 only RECORD predictions into m_arrowSignalCache -
pass 1's own comment says "NMS on: record only - the era-end sweep is the SOLE
renderer, so no raw (un-declustered) arrow is ever drawn mid-era" - and
PruneDirectionalClusters runs once, at the end of pass 3, which is the end of the
era. The requested behaviour was already the implemented behaviour.

So the arrows vanishing at the era boundary is not a timing fault. That sweep
DELETEs the arrow on any bar the era scored Neutral, and the model is currently
scoring Neutral on 98-100% of bars (see the logit-adjustment finding: Neutral is
the RAREST class at 10.6% and the imbalance correction is subsidising it by ~1.2
logits). The chart is reporting the model accurately; the model is the problem.

One thing that CAN clear arrows at era 0, and did on the first attach after the
|ALTW re-key: ClearPersistedChartSignals("fresh topology at era 0 - arrows belong
to a previous model"). That fires once per fresh model, not per era.

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:41:59 -04:00
AnimateDread
b19799910d fix(ui): chart arrows follow the BEST checkpoint, not the latest era
User report: "as soon as the next era training begins the chart signals are
erased, they should persist for as long as they are accurate."

Cause: PruneDirectionalClusters ran unconditionally at the end of pass 3, and it
DELETEs the arrow on any bar the CURRENT era scored Neutral. A model exploring
away from its best therefore wipes the chart every era even though the best
checkpoint still calls those turns. On the run that prompted this the model sat
at OOS recall Neutral:100% for 40+ consecutive eras, so essentially every arrow
was deleted at every era boundary.

The render is now deferred to the era-end block - the first point that knows
whether the era beat the best checkpoint - and only a new best repaints. On eras
that did not improve, the previous best's arrows stay untouched. Two exceptions
keep the chart from ever showing nothing: before the first checkpoint exists
there is no best to preserve, so early eras still paint; and a finishing run
repaints unconditionally, because FinalizeTrainRun is about to restore the
deployed weights and the chart must describe THOSE.

Recorded for ensemble members too. A member's own best era is not the deployable
one (the joint checkpoint decides that), but it is still the most accurate thing
that member has drawn, and the alternative is a chart that empties itself.

Also verified against the log, since two other symptoms were reported alongside:

  era cadence  PAI-b6b5 (before these changes) 3.76 s/era
               PAI-17ae (after)                3.53 s/era
  topology     both "2 dense from 16 units | input 800 (16 bars x 50)"
  calibration  both fitted on 1699 held-out bars

So training speed is unchanged - the ~6% is the ranking slice removing 20% of
pass 3's bars. It only FEELS fast because this is a single PAI chart taking the
whole 120 ms budget, not four ensemble members sharing it behind an era barrier.

The Neutral collapse is also pre-existing, not new: b6b5 ran at Neutral 94-100%
with 1-5% directional calls for all 723 of its eras, before any of this work.
That is the known neutral-collapse/recall-gate failure mode, and it is what
"barely drawing signals" actually is. Worth watching, separately: b6b5 reached
best-bal 34.2% by era 723 while 17ae is at 13.6% after 77 - too early to read,
but it is the number to check once 17ae has run comparable eras.

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:33:55 -04:00
AnimateDread
ee3682d949 fix(features): collapse only the anchor's own run - leave lagged readings put
User's call before deploy: "I would rather avoid lagging so the NN finds
accurate patterns." Correct instinct, and it picks the conservative variant.

110b384 deduplicated the WHOLE window, so every distinct reading survived at one
slot. The flaw is which slot: it depends on where the calendar-day boundary falls
inside that particular window, and on H4 that boundary cycles through ~6 phases.
A dense layer holds a separate weight per (slot, feature), so a given lag would
have landed on a different coordinate from one window to the next - turning a
stable lagged input into a moving one.

Now it blanks only bars carrying a BYTE-IDENTICAL copy of the anchor's reading
and stops at the first bar that differs. An as-of lookup into a daily file is a
step function in time, so those copies are exactly the contiguous run of bars
sharing the anchor's calendar day. Everything older keeps its natural replicated
run, in the same slots it always occupied - whatever the net learned to read
there, it still reads there.

Why the anchor's reading is the right one to isolate: the window's newest slot IS
the bar being predicted (BuildFeatureWindow's final iteration lands on r, and
pass 3 grades that same index), so it is the reading contemporaneous with the
decision - and the only one the alt screens ever validated. They measured the
CURRENT reading's MI against forward range and never tested lags, so the lagged
content is unproven, which is a reason to leave it undisturbed rather than a
licence to rearrange it.

What is still fixed: the anchor's reading reaches the first layer on one
coordinate instead of once per bar of its day, removing the ~16x gradient
upweight for the validated signal. And this is IDENTICAL to full dedup exactly
where replication was worst - on M15/H1 the whole window sits inside one calendar
day, so the anchor's run is the whole window - and a no-op on D1, where the bar
before the anchor is already a different day and the loop breaks immediately.
The two differ only on middle timeframes, and there this is the safe side.

Fingerprint |ALTW:1 -> |ALTW:2 so nothing trained under the hour-old full-dedup
semantics can silently resume under these.

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:21:22 -04:00
AnimateDread
110b38470a fix(features): dedup the alt block BY VALUE - aba9bd2 broke D1 charts
aba9bd2 kept only the newest slot's copy of the external block. That is right on
every intraday timeframe and WRONG on D1: there the 16 window bars are 16
distinct calendar days, the daily alt file returns a different row for each, and
blanking 15 of them destroyed real information instead of a copy of it. Caught
while extending the measurement to the other instruments.

Now compares values instead of slot positions: walk newest -> oldest, keep the
last DISTINCT reading, blank a slot only when it repeats one a newer slot
already carries. Exact on every timeframe with no timeframe test, and it also
handles weekends, holidays and publication gaps, where a window spans fewer
distinct rows than calendar days. How much collapses falls out of the data:

  M15 x 16 bars = 0.17 calendar days -> 1 distinct row  -> 15 of 16 blanked
  H1  x 16 bars = 0.67 calendar days -> 1 distinct row  -> 15 of 16 blanked
  H4  x 16 bars = 2.67 calendar days -> ~3 rows         -> ~13 of 16 blanked
  D1  x 16 bars = 16   calendar days -> 16 rows         -> NOTHING blanked

OTHER INSTRUMENTS - the question that prompted this. Per-symbol exports for
USDJPY/XAUUSD/EURUSD are not on disk (written only when that chart is attached),
but they are not needed: CAltData reads a DAILY file for every symbol, so bars
sharing a calendar day are byte-identical by construction everywhere. What
varies per symbol is only WHICH sources, and the catalog (AltDataFetch.mqh
AddSpec rows) gives:

  SP500  13 = risk(3) + cot_spec_net(1) + eia(3) + mac(6)
  EURUSD 15 = cot(3)  + risk(3)         + eia(3) + mac(6)
  USDJPY 15 = cot(3)  + risk(3)         + eia(3) + mac(6)
  XAUUSD 14 = risk(3) + ivol(2, GVZCLS) + eia(3) + mac(6)

Measured observation-date gaps in the raw sources on disk: VIX, USD index,
DGS10, T10Y2Y, T5YIE, DFF, ECBDFR all 1 day; COT and EIA 7 days; CPI and UNRATE
31 days. NO per-bar source exists anywhere in the catalog - the tick-activity
survivors from the flow screen are an in-terminal feature block, not alt data,
and are untouched by any of this. So the redundancy is universal across
instruments; only its magnitude varies, and by timeframe rather than by symbol.

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:11:52 -04:00
AnimateDread
aba9bd2bea perf(features): the external block enters the window once, not once per bar
Measured on the live SP500 D1 export (6073 rows, 13 features, 5888 simulated
16-bar windows):

  distinct values per feature per window : 1.7 - 2.7 of 16 slots
  variance in the first 13 PCs          : 96.5 - 97.0%
  components for 95% / 99%              : 12 / 17-19
  effective rank (entropy)              : ~11.5

208 inputs carrying about 12 dimensions. Only 6 of the 13 features move daily
(VIX complex, USD, the rates trio); 5 are weekly (COT, EIA, output gap) and 2
monthly (CPI, unemployment). The lookup is as-of by bar open time into a DAILY
file, so bars sharing a calendar day are byte-identical by construction.

The cost is NOT overfitting capacity - collinear copies span ~12 directions,
not 208, so an earlier claim that this wasted 26% of the model overstated it.
It is GRADIENT WEIGHTING. Batch norm standardizes each of the 208 coordinates
independently; that rescales the copies without decorrelating them, so one
factor arrives on 16 unit-variance coordinates, each weight takes a full-size
step, and the factor's aggregate coefficient moves ~16x faster than a per-bar
price feature's. The network was biased toward the external block by a factor
of the window length - and pointing the wrong way, since these features cleared
only a marginal incremental screen while price is the base signal.

Zeroed at WINDOW ASSEMBLY, not in BufferTempData: that output is cached PER BAR
and a bar sits at slot 15 of one window and slot 0 of the next, so a
slot-dependent value there would poison the cache or force a recompute per slot.
The cache keeps true values; only this window's copies are cleared. Width
contract untouched - same count, same positions - so conv/LSTM/HYBRID keep their
bar-major rectangle unchanged and the block arrives at the newest bar, which for
the LSTM is the final timestep. Zero-variance coordinates are safe through batch
norm (divisor is MathMax(MathSqrt(var + BN_EPSILON), BN_MIN_STD)).

Fingerprint gains |ALTW:1 when alt data is on. Same width and same .cfg, so
nothing else would have caught a model trained under the replicated layout
resuming under this one. Conditional append per the existing rule: configs
without alt data keep their fingerprints and their trained models.

NOT the concat branch. CNet is a strictly linear stack (CLayerDescription has no
input-source field; NetBuild wires i to i+1 and stores layer L's weights on
L-1), so a real two-tower model needs a new multi-input layer type across
WarriorCPU, WarriorDML and the OpenCL kernels plus an .nnw format change - the
highest-risk change in this repo, in the code that produced the transposed dense
gradient, the Adam second-moment bug and the reversed LSTM window. This captures
the part of that idea the measurement actually supports, at no engine risk.

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:05:10 -04:00
AnimateDread
7caf2f626e feat: derived taper restored; DB ranking reads a reserved slice, shrunk
TOPOLOGY - reverts the two constants and drops CausalHiddenLayerFloor.
The MQL5 article's 30%-per-layer cut and floor of 20 are load-bearing on ITS
first-layer width of 1000 (1000->300->90->27 needs a floor to stop). This
codebase MEASURES that width, and on the live SP500 H4 config it is 16 units -
already floored, with the budget printing "11360 estimated in-sample bars
cannot support a 800-wide input ... roughly 1.1 weights per training bar -
expect overfitting". At 16 units a floor of 20 makes lastHidden >=
m_initialNeuronsCount, so ComputeHiddenLayerCount returns on its first branch
and the width taper - the only part derived from this symbol's data - became
dead code on all four ensemble members, with depth (2 -> 4) set entirely by
counting feature domains. ComputeLayerWidths had already rejected this exact
pair of constants in its own comment.

The causal floor's premise does not hold either: layers are not inference
steps. The "1 layer linear / 2 nonlinear / 3 multi-connected" result is
Lippmann 1987 and is about hard-threshold units; with sigmoid/ReLU, Cybenko
1989 and Hornik 1991 give universal approximation from a single hidden layer.
Depth buys parameter efficiency for compositional functions, not reasoning
hops. ForceHiddenLayers remains for measuring depth directly.

RANKING SLICE - the backfill no longer reads the window it is judged on.
The deployed checkpoint is CHOSEN as the best-scoring era on the OOS window,
so win rates measured back over it are selection-inflated, and the backfill
was writing exactly those into the table filter weights rank on: the
selection set consumed twice, beside a deploy gate that applies a Sidak
correction for that effect. The newest RANK_SLICE_PCT_OF_OOS (20%) of the OOS
window, plus a label-horizon purge, is now reserved and graded by nothing -
not pass 3, not checkpoint selection, not the gate. The backfill reads only
that. The gate keeps ~80% of its measurement (power goes as the square root,
so ~10% of a sigma), and the slice is the newest data, which is the regime
about to be traded. RankSliceBars returns 0 when no honest slice fits and the
backfill then REFUSES and says so, rather than falling back to the scoring
window and looking like a success.

SHRINKAGE - per-tier win rates are shrunk toward the filter's own pooled rate
by MIN_TRADES_FOR_WIN_RATE pseudo-trades before becoming weights. The raw
ratio at the minimum sample count carries a ~15pp standard error, so a tier
that went 8-2 was handed weight 80 and outranked a tier measured over
hundreds of calls at 55 - the ranking was being driven by which small tier got
lucky. Opt-in per call site (priorWeight 0 keeps the raw behaviour).

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:49:52 -04:00
AnimateDread
b6736fd40b fix: the DB backfill could never run, and HEAD did not compile
Four defects in 64c5dd5/1a05e63, found by review + a baseline compile.
Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable.

1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were
   declared `virtual bool ... override`, but CAppDialog declares both as
   `virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151
   on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was
   never a success flag to forward. Verified: 0 errors, 0 warnings.

2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual
   simulation (that one has been dead since it was written). Both are armed
   at the instant convergence is declared, and both advance only from inside
   Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick
   ArmStudyEvent site sits in the `else` of a branch taken whenever
   m_trainingComplete is set and m_trainRunActive is clear - which is exactly
   the state FinalizeTrainRun() leaves behind one line before they are armed.
   Train() was never called again, so the walks sat at their start index
   forever: no "simulation complete" line, and not one row written to the DB
   this feature exists to fill. Only a manual Resume/Retrain unstuck them.
   Both flags now keep the model schedulable.

3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed.
   Ensemble members deploy at Train() ENTRY and return immediately (so no era
   is wasted), which skips the era-end block the backfill was started from.
   All four members were a no-op for a second, independent reason. Armed on
   the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff.

4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key,
   no duplicate check - and m_dbBackfillDone is in-memory, so every later
   attach that retrained to convergence wrote a second full set of rows for
   the same bars. The ranking would count one bar once per model that ever
   deployed, weighting superseded opinions as heavily as the live one. A
   .dbfill marker stamps the deployed era; written only on completion (an
   interrupted walk redoes itself rather than ranking a partial window) and
   deleted with the other sidecars on reset-weights.

Also: WarmBlocking's timeout was silent, which restored the exact silent
pin failure it was added to prevent - it now says so in the journal, and
returns true for "no reference pairs to wait for" so the warning stays rare
enough to be read.

Not addressed, needs a decision: the backfill scores the OOS window with the
checkpoint that was SELECTED as best on that same window, then writes those
win rates into the table filter weights rank on - the selection set consumed
twice, undiscounted, while the deploy gate right next to it applies a
family-wise correction for exactly that effect. The rows are also simulated
triple-barrier outcomes at today's spread sharing a table with realised
fills. The completion log line now states both plainly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:25:51 -04:00
AnimateDread
64c5dd55d3 feat: implement one-shot pattern-database backfill and enhance accuracy tracking for ensemble models 2026-08-16 21:08:41 -04:00
AnimateDread
5a5be8999e fix(altdata): add late warning for alt data arrival after model build 2026-08-16 20:04:13 -04:00
AnimateDread
e049b624ba feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").

Four decisions move from the member to the ensemble:
  * which era is "best"    -> the era whose COMBINED VOTE scored best
  * what is checkpointed   -> a JOINT snapshot: every member's weights
                              at that one era
  * when the run gives up  -> one shared plateau ladder
  * whether it may deploy  -> family-wise gate on the vote

WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.

Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.

Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.

Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.

Verified: full MetaEditor compile, 0 errors 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
AnimateDread
b77e7b4766 fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:

1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
   id 1 and handled id 1001, and CExpertCustom broadcasts every chart
   event to every filter - so each posted event ran a train chunk in ALL
   N members (N*N chunks per round) and the chart thread never idled
   long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
   OnChartEventHandler. Fix: per-instance study-event ids
   (STUDY_EVENT_ID_BASE + construction order, offset above the Controls
   library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
   double-clicks fired training chunks). ArmStudyEvent() is the single
   post site; lost-event watchdog replaces the accidental
   sibling-clears-my-flag rescue.

2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
   identical features/labels, and it ends in the full MI diagnostic
   suite, which the MI-share gate never intercepted on the sweep path -
   four members ran four identical ~36s sweep+report blocks. First
   member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
   the rest apply it and skip both.

3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
   log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
   autosave in flight ate it, OnDeinit got ~430ms and died in the first
   member's arrow persist ("Abnormal termination" 432ms in). Fix: early
   visible-UI sweep (native prefix deletes for status/panel/dialog)
   right after ClearStatusLabel, and a fast path for still-training
   models - their arrows are re-rendered every era, so they get one bulk
   purge instead of scan+atomic-write in the death window.

4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
   by era together; a member ahead of the slowest still-training member
   declines Train() calls and its chunk budget is donated
   (TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
   COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
   adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
   the last member to finish the era scores the averaged vote vs the
   mirrored Min_Vote_Open against the same target-before-stop outcomes
   members grade themselves on, publishing an "Ensemble vote" line on
   the aggregated panel. Member headlines now carry their lifetime win
   rate with break-even.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00