Commit graph

555 commits

Author SHA1 Message Date
AnimateDread
38a12a240b refactor(kiss): drop the AI sub-vote early-exit route; certified == traded
First of the AI vote layers to go. CheckClosePosition had two exit routes:
the stock blended vote, and an AI-only one reading the AI members' sub-vote
undiluted. The second existed because an AI reversal averaged in with the
classic filters could be diluted below the threshold before it could close
a position.

It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair
Direction() carried to feed it.

This CLOSES the certified-vs-traded gap rather than widening it. The
deploy gate certifies a win rate measured on hold-to-resolution outcomes,
and CheckClosePosition already gated the blended route off whenever an AI
model's derived geometry was on the order - so the AI route was the only
vote exit an AI-certified trade could take, and the exit replay existed to
reproduce it. With it removed, an AI-certified position holds to its
barrier by construction instead of by reconstruction, so Warrior_EA.mq5
now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded
Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the
simulated exit off by arithmetic - correct at the shipped default, and one
input change away from the simulation and the live path describing
different games.

Min_Vote_Close keeps its meaning for the classic route and is now
documented as inert wherever an AI certificate governs, rather than
appearing to drive an exit it can no longer reach.

Comment debt cleared while here: a tombstone block for m_ai_exit_threshold
(a member deleted 2026-08-18) still sat in the header, and four sites still
named LiveSignedConfidence's "two consumers" - it had one, the intelligent
trailing stop, since that same date.

NOT touched, and deliberately: NMS declustering is NOT a quality layer. It
gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the
undeclustered population is ~8x what the EA trades. Removing it would
multiply live position count, not simplify a scoring path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:27:28 -04:00
AnimateDread
77594ef5fb refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.

  System\AltData.mqh          column median  -> MathMedian (exact, no change)
  AIBase\Labels.mqh           swing median   -> MathMedian
                              leg-range med  -> MathMedian
                              stop ladder    -> MathQuantile, read in one call
  AIBase\Topology.mqh         window median  -> MathMedian
  AIBase\AutoTune.mqh         MI terciles    -> MathQuantile + MathMin/MathMax
  Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()

gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.

VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.

Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.

Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
AnimateDread
29c82ad50b refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.

  BinomialVar(p, n)                      p(1-p)/n
  BinomialSEPct(p, n)                    100*sqrt(p(1-p)/n)
  BinomialCallsForEdge(p, edge, sigmas)  the same, solved for n
  NormalUpperTailQ(z)                    Q(z), via Math\Stat\Normal.mqh
  SidakFamilyP(z, N)                     1-(1-Q(z))^N

Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".

CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.

What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
AnimateDread
0826f8b900 refactor(dry): one definition of "usable quote" in the pre-trade checks
Six checks each fetched bid/ask and rejected a non-positive pair with their
own wording. TCLiveQuote() now owns that rule, so what counts as a usable
quote is defined once and every rejection reads the same way. The one
message that said only "no live quote for <symbol>" now names what it was
about to do, like the other five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:33:19 -04:00
AnimateDread
ea2552efe2 refactor(stdlib): adopt Math\Stat for the deploy gate's normal tail; retire the b1/b2/lr/momentum macros
The gate's NormalUpperTail was a hand-rolled Abramowitz & Stegun 26.2.17
approximation. Its own comment gave the reason - "drags a chain of headers
behind it" - and that turned out to be one file: Math\Stat\Normal.mqh
includes only Math.mqh, which includes nothing. Swapped for Cody's rational
approximation in the library (~18 significant digits vs |error| < 7.5e-8).
No past verdict changes: at the z the gate operates on, the difference is
orders of magnitude below DEPLOY_FAMILY_WISE_ALPHA.

Adopting it needed the four bare macros in AI\Network.mqh gone first.
"#define b1 AdamBeta1" collides with an identifier in Math.mqh, so the
include would have macro-expanded the library's own local and failed to
compile - the same landmine that made the original author rename the
approximation's coefficients to ntB1..ntB5 rather than use the reference's
b1..b5. lr, b2 and momentum are the same class of hazard: single-token
global macros in a 52k-line codebase. All four now resolve to the input
names they always aliased, which is a pure textual identity - verified zero
bare occurrences remain.

Also:
- SelectionSort over the buffered signals was O(n^2) with an O(n^2) count of
  StructToTime calls, because the comparison rebuilt both datetimes from the
  six int date fields every time. Now materialises the keys once and does an
  insertion sort; ArraySort cannot permute a struct array. IsEarlier goes
  with it, MakeDateTime becomes SignalTime.
- Seven FileOpen sites lacked FILE_SHARE_READ|FILE_SHARE_WRITE, including
  AtomicWriteBegin, which stages every model save. All 43 sites now carry
  them - an exclusive open fails outright when another process holds the
  path, which here has meant a silently skipped save.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:31:36 -04:00
AnimateDread
b0fec27779 merge: built-in indicator migration + YAGNI accessor pass
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:20:00 -04:00
AnimateDread
61c0d19ca9 feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift
MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on
both consumers - the classic vote and the NN MA input feature. This drops the
five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent;
MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all
four. It also removes a documented failure mode: a custom indicator's depth is
bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS
the bar on a short read - the "feature 25 fails on every bar" incident of
2026-08-17. A built-in is served at any depth.

MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning.
SanitizeMaType() is the single validity rule; TunedPeriods records now carry a
version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a
stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid
new codes). Existing .nnw files re-key on their own, because MA_Type is hashed
into the topology fingerprint, so models retrain rather than silently running
on different MA values. EXPECT A FULL RETRAIN.

ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag -
verified by normalising identifiers and stripping comments, 233 significant
lines each with only renamed symbols differing. It now loads the stock one, so
nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both
#resource entries are gone.

Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 =
forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom,
inherited by all four rather than repeated per module. Defaults to a sentinel
meaning "unset", so the AI signals and the aggregate keep the stock every_tick
rule and their feature/label alignment is untouched. The META corpus sweep still
takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a
real override, not the name-hiding the old comment claimed.

Not compiled - MetaEditor compile pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
AnimateDread
552edb5fb1 refactor(yagni): drop 13 accessors nothing called; unify the ATR trailing pair
Verified dead by grep across all first-party sources (references/, Scripts/,
research/ excluded): EraCount, HiddenLayersCount, LstmHiddenSize, ConvFilterCount,
HistoryBars and MinTrainYear setters, PendingBatchSamples, getPrevOutIndex,
BaseCurrency, QuoteCurrency, CurrencyCount, IsLoaded, LastFiredDirection,
DBConfidence, SpecIndex, and the conv Step/WindowOut shape accessors. Every
backing member stays - each is still read internally and several are pinned by
the positional .cfg layout - so this removes surface, not behaviour.

Two comments were asserting the opposite of the code and are now true: the
"No setter: the taper's endpoints are derived" note was directly above three
setters, and the conv shape block claimed EnforceTopologyContract reads all
three accessors when CNet::FirstConvWindow only ever calls Window().

CTrailingATR::CheckTrailingStopLong/Short were byte-identical but for Bid vs Ask
and the isLong flag; both now delegate to one CheckTrailingStop body.

Deliberately NOT removed: the fractal-target branch (TrainTargetFractal,
IsFractalTarget and their label machinery). It reads as dead because the
TrainingTarget input was withdrawn, but Warrior_EA.mq5:836 documents it as a
parked option with a three-line restore path - that is a product call, not a
refactor.

Not compiled - MetaEditor compile pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 18:55:36 -04:00
AnimateDread
2be2970434 fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.

EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.

SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.

The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.

Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.

Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).

NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
AnimateDread
caad156464 fix(geometry): the ratio raise was reported against reachability, not bounded by it
First clean derivation after 6f2def0 produced stop 1.22 / target 4.86 = 1:4 on
SP500 H4, and the labels came out Buy 4.3% / Sell 6.1% / Neutral 89.6% - a 20.7:1
imbalance, against 3.4:1 at 1:2 and 1.5:1 at 1:1 on the identical 10601 excursions.
That is the class-collapse regime, not a geometry.

c3daded proposed ONE leg-implied ratio per rung and left the reachability floor to
REJECT the rung, with a comment claiming the raise was "bounded" by it. Rejection is
not a bound. The 5.09*ATR median leg proposed 1:4 at every stop, all seven rungs then
missed their own floor (0.7%-9.6% against 12-20%), the no-rung-clears fallback fired
and re-proposed the same 1:4 at the tightest rung - and printed a WARNING predicting
exactly the rare positive class that followed. The system diagnosed itself correctly
and had no authority to act on it: the same shape as the scan-vs-deriver split
c3daded fixed one layer up, reintroduced one layer down.

Why the full leg overshoots: a ZigZag leg is pivot-to-pivot travel and an entry is
not a pivot - it lands inside the leg, with roughly half of it left on average.
Sizing the target at the whole median leg asks the market to deliver, from an
arbitrary bar, the entire move it usually makes between extremes.

Rather than assume the half and hard-code a factor, the raise now steps DOWN the snap
ladder (5 -> 4 -> 3 -> 2.5 -> 2) until the first-passage ladder says the target is
actually reached, stopping at the 1:2 policy floor because that is risk policy and
not a measurement. The legs still raise the ratio wherever the travel supports it;
the market decides how far. Applied in BOTH the rung loop and the fallback branch -
omitting the fallback is what actually shipped the 20.7:1 labels, since that branch
runs precisely when no rung was reachable. Rung rows now print
"[legs proposed 1:R, unreached]" so a walked-back raise is visible as one.

Verified: BarrierStepDownRr is strictly decreasing and bottoms at the floor, so both
loops terminate; rung-row StringFormat re-counted at 18 specifiers / 18 arguments.

NOT COMPILED - user compiles in MetaEditor.
2026-08-19 17:04:32 -04:00
AnimateDread
6f2def0605 fix(geometry): a weights reset could never change the barrier - the pair laundered itself through the wipe
Reported as "it still seems leaned towards 2x atr" after a full Delete && Reset
Weights on SP500 H4. It was not the .cfg pin, and it was not the new ratio floor
failing to take: the geometry is held in members ResetWeights never cleared, so
a reset wiped the .cfg, built a fresh net, restarted at era 0 - and then relabelled
under the PREVIOUS model's pair, before SaveTopologyConfiguration wrote that stale
pair back into the brand new .cfg. Reset, re-derive, re-pin, with the middle step
missing. No number of resets could ever have moved it.

Evidence in the 2026-08-19 journal: RESET WIPE at 16:23:38.699 (.cfg=deleted),
"rebuilt a fresh topology" at .790, and a label cache at .890 with a distribution
byte-identical to the pre-reset 2.00/2.00 one (Buy 3221 / Sell 3303 / Neutral 4837,
mean lifespan 6.4 bars) - 100 ms later, with no DeriveBarrierGeometry between them.

The member that actually blocked it is m_geometryDerivePasses.
LoadAndCompareTopologyConfiguration pins it to BARRIER_DERIVE_MAX_PASSES to block
the fixed-point iteration, which is correct for a LOAD - an existing model must
never re-derive or the target moves under fitted weights - and exactly wrong for a
RESET, which is the act of declaring there are no fitted weights left to protect.
State that is correctly sticky for one lifecycle event, silently inherited by
another; the same shape as the .cfg pin sitting beside it.

ResetWeights now returns the whole derivation to its constructor state: the three
latches, the pair, the horizon and its flags, the swing/lifespan measurements, the
scan's own outputs, and m_spreadAtr - that last one matters because the cost filter
is deliberately inert on pass 1 (m_spreadAtr <= 0.0) and an inherited spread makes
a reset model walk a different ladder than a genuinely new one. m_sl_mode/m_tp_mode
go back to the SL_Mode/TP_Mode inputs, since the adopt path overwrites them in
place with no copy of what was configured.

NOT COMPILED - user compiles in MetaEditor.
2026-08-19 16:31:18 -04:00
AnimateDread
c3daded397 feat(geometry): 1:2 becomes a FLOOR the swing legs may raise, and the scan can no longer undercut it
Two coupled changes, both from measurements in today's SP500 H4 log.

1. THE SCAN WAS OVERRIDING THE DERIVER ON THE WRONG OBJECTIVE.
   At 13:37:47 DeriveBarrierGeometry produced stop 1.21*ATR / target 2.41*ATR - break-even
   33.3%. Thirty-seven seconds later the barrier-geometry scan adopted 2:2 - break-even
   50.9% - because it carried 0.0143 nats of entry-time information against the configured
   pair's 0.0075, and cleared its family-wise gate. Information is not expectancy, and the
   scan says so itself; nothing checked what the adoption did to the operating point. It
   did this: the fitted thresholds immediately after read 38.8% win vs 50.9% break-even
   (-12.2pp) and 48.3% vs 50.9% (-2.6pp), where the earlier model on this instrument at a
   1:2 geometry had fitted +1.8pp. The deriver applies the ratio as user RISK POLICY; a
   scan that can crown 1:1 makes two subsystems disagree about one geometry - the same
   split this file already fixed once for the clamped-horizon rule. The scan now enrols and
   crowns only pairings at or above the floor; sub-floor pairs are still scored and printed
   (marked 'r') so the choice stays auditable. This is NOT the min-RR rule removed on
   2026-08-09 - that one guarded a rejection filter that no longer exists.

2. THE RATIO IS A FLOOR, NOT A CAP (user: "the ratio of 1:2 is a minimum that I want, but
   it should not cap to that if the average zigzag moves gives more room").
   BARRIER_TARGET_RR -> BARRIER_TARGET_RR_MIN. ComputeBarrierHorizonBars already scanned
   ZigZag pivots for leg DURATION; it now harvests leg RANGE in the same pass - two
   properties of one object, so the horizon and the target describe the same legs instead
   of two windows. The per-rung ratio is the floor raised toward median-leg/stop, snapped
   DOWN to a coarse ladder (2.0/2.5/3.0/4.0/5.0). The ladder is coarse on purpose:
   PooledGate pools only instruments whose structural break-even matches, and continuous
   per-instrument ratios would never match and would silently empty the pool.

   A leg is the right yardstick precisely because it owes NOTHING to the barrier - sizing a
   target off travel measured over the barrier's own horizon is the circular loop that ran
   EURUSD/USDCAD away to 14-31*ATR in 2026-08-07. The raise stays bounded by the three
   tests already in the ladder: reachability, the horizon ceiling (first-passage time grows
   with stop x target), and the cost fraction.

   Consequential fixes: the reachability floor was a macro keyed to the fixed ratio and is
   now BarrierMinReachPct(rr) evaluated per rung (a raised ratio has a lower break-even, so
   a fixed floor would be the wrong strictness); the detectability break-even likewise;
   PooledGate now writes and matches the ACTUAL ratio (TargetRR()) rather than the floor.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:16:33 -04:00
AnimateDread
888f32d21c fix(gate): the plateau shortcut re-ran the deploy test every era, raising its own bar
User report: 'eras since best' in the ensemble line is always 0 (era 147, best at era 90,
'0 eras ago'). That is a control-flow bug wearing a display symptom.

Once every member's in-sample error had plateaued, the shortcut forced the ladder to its
DEPLOY stage on EVERY era. The failed-gate branch resets the stage to 0 so the ladder can
climb again - so the shortcut raised it, the branch cleared it, forever. Three consequences,
only the first of which was visible:
  - g_ensErasSinceBest was reset every era, pinning the counter at 0.
  - The stage-1/2 boosted warm restarts were never reached, so the one mechanism that can
    un-plateau a stuck member never ran. The models sat at a WORSE error than their best
    (0.2408 -> 0.3015 on PAI) with no escape.
  - Every repetition ran EnsembleSurvivesSelection against an unchanged best and incremented
    the candidate-era count the family-wise correction divides by. The run spent its time
    RAISING ITS OWN SIDAK BAR - the same waste as the 2026-08-18 inert IS-error stop, one
    layer up, and the reason a gate that needed >47.8% saw its bar climb era after era.

Fix: the shortcut fires ONCE PER BEST-ERA (g_ensGateTestedEra, stamped before the outcome
branches because it is the re-running that inflates the family, pass or fail). A refused
gate now falls back to the normal counter-driven ladder - warm restart, anneal, then a
fresh deploy test - which is the escape the shortcut was skipping.

Also, per user: the signal marks were too small to see. Span doubled (2.6 bar widths, so
the overhang either side of the candle is ~0.8 bars) and both layers thickened - 1px dotted
was invisible on a candle chart at any realistic zoom.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:40:52 -04:00
AnimateDread
9a0d063da4 fix(inputs): the Neural Networks group header was singular
Typo, and more wrong than it was: the group now holds four independent NN toggles rather
than one architecture selector.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:23:37 -04:00
AnimateDread
5e0317f09d feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle
User request: 'move from arrows on lows and highs to small horizontal lines at the actual
prices the entry/exit would trigger, just a bit larger than the candles. dark green for
buy, dark red for sell.'

Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off,
spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually
fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's
LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow
glyph would clear the candle. The tooltip now carries that price too.

COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red).
Layer moves to width+style - the traded vote is solid and thick and drawn in front, a
single model's raw opinion is thin, dotted and behind the candles - which keeps the
distinction the old palette existed to draw (a model's opinion must never read as a trade)
while freeing colour to say one thing consistently.

Consequences handled, all of them the same 'a typed scan went blind' failure:
- SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now
  filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old
  217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load.
- AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path
  uses, so a restored mark and a fresh one are identical objects.
- The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently
  deletes nothing.
- ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type
  that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click;
  it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the
  missing check - trend lines are the most hand-drawn object there is.
- DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no
  caller can hand it a price it no longer draws at.
- Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only
  three lines above the note explaining it had been widened to every type.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:11:49 -04:00
AnimateDread
70eaa8a498 feat(direction): Intelligent becomes the shipped default for Trade direction
User request after the first ensemble run under the per-NN build: the drift verdict was
measured and printed every era while the input sat at BOTH, so it gated nothing - the
worst of both, because an authoritative-looking log line described a policy that was not
in force.

Safe as a default: the verdict fails open to BOTH (it drops a side only when the gap
clears 2 combined SEs on the overlap-deflated sample AND the weaker side sits below
cost-adjusted break-even), so an instrument with no measurable drift behaves exactly as
before. Verified it appears in no fingerprint or DB key - trade policy, not label
definition - so this changes no model identity and resets no training.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:02:56 -04:00
AnimateDread
6717509c8b fix(build): four compile faults - one was a SILENT enum collision that inverted the direction policy
Reported by the user's MetaEditor compile of f64e0f8 (26 errors, 4 warnings). The four
warnings mattered more than the errors.

1. INTELLIGENT WAS TWO ENUMS. MONEY_MANAGEMENT_STRATEGY::INTELLIGENT (=1) is declared
   BEFORE TRADING_DIRECTION::INTELLIGENT (=3) in InputEnums.mqh, so MQL5 resolved every
   'tradingdirection == INTELLIGENT' to the MM member and converted it to value 1 =
   TRADING_DIRECTION::LONG_ONLY. Wrong in both directions at once: selecting Intelligent
   (3) matched NOTHING and silently traded both sides, while selecting Long only (1)
   matched and handed the decision to the measured drift verdict - which can answer
   SHORT_ONLY, so the one setting that must never go short could have. Reported by the
   compiler as a WARNING only, never an error. Renamed to DIRECTION_INTELLIGENT; the
   VALUE stays 3, so saved .set files are unaffected. Swept every enum in the repo for
   sibling collisions (38 enums, detector validated against the pre-fix source, which it
   flags): none remain.
2. g_warriorMetaGate sits above the class it points at - added the forward declaration,
   the same pattern g_warriorEnsemble already uses in ExpertSignalAIBase.mqh.
3. The broker-time rename (b63e39f) never reached BufferNewTickSignal's PARAMETER or its
   two call sites: the local became brokerTime, the parameter stayed gmtTime, and the
   body was rewritten to read brokerTime. All five sites now agree.
4. ConfigureAISignal calls IsMetaTarget() from a free function - moved it to the public
   section (identity, not an implementation seam); the other meta seams stay protected.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:16:45 -04:00
AnimateDread
f64e0f8b67 feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate
User design (2026-08-19): 'remove the enum menu that selects neural networks... individual
inputs for every NN just like classic signals... the META NN should be integrated into the
voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.'

- AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/
  Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to
  any subset because the consensus divisor is the enabled capable weight. Two or more
  enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so
  existing weight files keep loading); one = the old solo preset; none = classic-only.
- Use_MetaLabeling un-couples META from the direction NNs (the old selector made them
  mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry
  (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR;
  pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and
  vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly.
- COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the
  meta target - solo-only until today, a trained META would otherwise sit in the consensus
  divisor as a permanent abstainer and shrink every vote by its module weight.
- CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same
  g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own
  time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K
  unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class,
  calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not
  model the veto - the standing solo-gate caveat, documented at the input.
- DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType;
  DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member
  subsets get 100+bitmask, outside the legacy range) so no existing database re-keys.
  filterID becomes the enabled roster via one EnabledNNSummary().
- HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the
  armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not
  only when an entry happens to be proposed.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
AnimateDread
1445f175ce feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
   check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
   TCAdjustStops does at order time - the M5/tight-ATR case where live
   trades ran wider geometry than training measured. Current stops level
   stands in for history (like the spread); measured quantity, so it
   does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
   RESCANS the label cache and now runs at every era end beside
   RankTiersFromOos - era-cadence instead of waiting for rare full
   rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
   centres' civil clocks (London 08-16 Europe/London, NY 08-17
   America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
   centre's own computed DST rule (EU last-Sun-Mar/Oct, US
   2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
   server-vs-GMT offset (half-hour brokers included). Windows may wrap
   midnight in broker time - the interval test handles it. Replaces the
   EET-hardcoded anchors, which were correct on exactly one broker and
   got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
   the bars bound the error - a too-late assumed close meets no bars
   (zero error), a too-early one truncates conservatively (<=1h, never
   optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
   fires are not fired bars (certified == traded), the zero-skill
   reference uses only ACHIEVABLE baselines (always-short is not a
   strategy a long-only book can run), and one-sidedness BY POLICY is
   not degeneracy - the two-sided requirement applies only when both
   sides are allowed. Sell predictions keep their other jobs (exit
   triggers, consensus dilution) untouched.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
AnimateDread
b63e39f026 refactor(time): broker time throughout - and the GMT DB basis was already a live bug
User decision: "stick to the broker's time throughout the codebase and
analysis, session filter, programmed close time etc". Investigation
found the GMT choice was not just inconsistent but broken: live
journaling stamped DB rows with TimeGMT() while the online-learning
backfill stamped them with BAR time (server) - two clocks ~3h apart in
the same column. The newest-row duplicate guard compares them on one
axis, so a live row landing within the offset after a backfill row was
silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals
store: the only honest reset for a mixed-basis corpus.

- Direction()'s clock (stamps every journaled row, keys the per-second
  vote window): TimeGMT -> TimeCurrent, variables renamed so the name
  cannot lie about the basis.
- UpdateSignalsWeights' future-row bound: same clock as the rows.
- Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo
  2-11). The GMT anchors were backwards for an EET-family broker - such
  a broker follows European DST, so London is DST-STABLE in broker time
  and moved twice a year in GMT. Tokyo drifts 1h each European summer
  (no DST to track) - accepted, smallest error on offer. Also fixed:
  inTimeInterval ignored its datetime parameter and called TimeGMT
  fresh - a dead parameter hiding a hardwired clock.
- MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the
  GMT->server offset scan is KEPT because it measures rather than
  assumes - it pins 0 on new corpora and still resolves old ones.
- AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules
  are external UTC-anchored events; the as-of join maps them onto server
  bars downstream.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:37:44 -04:00
AnimateDread
1a46dfdad9 feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on
every call so DST and per-symbol schedule changes track themselves.

- WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the
  symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) -
  a vote can no longer fire into a closed book and collect a broker
  error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay
  unguarded - closing risk must never be blocked by a session boundary.
- CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires
  "Close-all minute" minutes before that day's LAST session close.
  Friday + Market close + xxH05 = flatten 5 minutes before Friday's
  actual close. Resolved identically in three places: the live executor
  (CExpertCustom::OnTick), the label walk's vertical barrier
  (NextScheduledCloseAll - the symbol's CURRENT table stands in for
  history; MT5 keeps none, and a fixed hour is wrong by more), and the
  fingerprint (the |CUT: token already carries hour=24, so switching to
  the dynamic mode re-keys the model exactly like any schedule change).

Training itself is deliberately NOT gated on market hours: weekend
compute is free and labels only ever exist on real bars - what the
session table gates is order placement and, via the close-all barrier,
what the labels may count as holdable.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:34:27 -04:00
AnimateDread
b43b676239 feat(fingerprint): an active close-all schedule keys the model identity
The schedule became part of the label's meaning (3e467f9): the same
chart trains a different target under Friday-23:45 than under
everyday-22:00. Without this token a schedule change silently resumed
weights fitted to the other target - the stale-enum-wrong-target family.
Active schedule -> "|CUT:day@hour:minute" in the fingerprint; disabled
schedule appends nothing (pre-change no-schedule models stay
byte-identical). Default-Friday charts re-key exactly once, at this
change - deliberate: their weights were trained on weekend-blind labels
and are confounded anyway.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:23:54 -04:00
AnimateDread
3e467f92e9 feat(labels): the scheduled close-all is now a vertical barrier in the label walk
User report: "I exit everything on Friday close to avoid weekend swap...
if the NN training thinks I hold over the weekend it could produce
inaccurate results" - it thought exactly that. TripleBarrierLabel walked
its full horizon (64 bars, mean lifespan ~18 H4 bars ~ 3 days) straight
through the scheduled flat, scoring trades the deployed EA is guaranteed
to have closed on Friday 23:45. SQX applies this rule when building
strategies; the EA's own labels did not.

NextScheduledCloseAll() mirrors CExpertCustom::OnTick's live check
exactly (same three inputs, same -1 disabled sentinels, same
CLOSE_EVERYDAY semantics, same server clock). The walk stops at the
first bar that does not END by the cutoff - OHLC cannot order the
tradable fraction of a partial bar, and ties go to the refusal, as
everywhere in this file. An unresolved trade at the cutoff times out to
Neutral, exactly as live would flatten it. Excursions, the first-passage
ladder and the label lifespan truncate with the walk, so the DERIVED
geometry is automatically sized to the tradable window - a target the
flat rule never lets price reach stops counting as reachable.

The prebuild census now splits timeouts: "horizon too short?" vs "ended
by the scheduled close-all" - different questions, different fixes.
Schedule disabled = no cutoff, exactly like live.

Models trained under weekend-blind labels are fitted to a different
target; charts with the close-all enabled (the default) should be reset
to retrain under the honest labels.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:17:14 -04:00
AnimateDread
348492fb3b feat(direction): INTELLIGENT trade direction - the measured drift picks the side(s)
SQX EdgeFinder precedent (user request): adjust for the drift instead of
fighting it. The 2026-08-19 telemetry found the models leaning SHORT
(Buy recall 21% vs Sell 40%) against a long-favored market (always-long
34.3% vs always-short 29.5% at the adopted geometry).

TRADING_DIRECTION gains INTELLIGENT = 3 (appended, explicit value,
.set-safe). It resolves at runtime from the label cache's per-side win
rates - the Buy/Sell shares ARE the win rates of taking every bar
long/short at the REAL stop/target with spread charged. A side is
dropped only when BOTH hold: the drift gap clears 2 combined SEs on the
overlap-deflated effective sample (EffectiveSampleSize - labels overlap
~18x), AND the weaker side sits below cost-adjusted break-even (a side
that still clears costs is kept; drift tilt alone is not a reason to
refuse a profitable side). Fails open to BOTH: unmeasured, tiny
effective n (<30), insignificant gap, or classic-only charts (no label
cache).

One resolution point - WarriorEffectiveDirection() - feeds all three
gates so they cannot drift apart: CheckOpenLong/Short (live entries),
the filtered-view sweep (a blocked side falls into the delete branch,
mirroring live), and the vote HUD's "-> TRADE" verdict. The verdict
re-derives at every label-cache rebuild, prints only on change, and is
computed even when the input is not Intelligent (marked informational).

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:10:05 -04:00
AnimateDread
cf9a9395ee fix(hud): the headline word is the DECISION, not the lean
"VOTE BUY 5.9%" against a 25% bar read as an ensemble that is
permanently long, when it is an ensemble that would trade nothing - one
member voting BUY at weight 7 against three flats owned the word all
day (reported 2026-08-19, screenshot). With the per-member lines now
showing each model's individual leaning, the top line says what the bot
would DO: BUY/SELL only at or above Min_Vote_Open, NEUTRAL below it
(including the all-flat read), "--" only when nobody has a decision.
The lean survives as a SIGNED percentage (+ buy side / - sell side).

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:24:21 -04:00
AnimateDread
b55880c57d fix(hud): sign-mismatch warning in the display-forward throttle
age is a uint tick delta; the ternary picking DISPLAY_FWD_ERA_MS vs
DISPLAY_FWD_MIN_MS is a runtime int expression the compiler cannot
constant-fold (unlike the bare-literal comparisons elsewhere), so the
comparison warned. The defines now carry the (uint) cast.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:07:12 -04:00
AnimateDread
f30e342a1d feat(logs): throttle the settled per-era diagnostics - measured 22MB/9.5h of confirmed-working systems
Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus
the excursion verdict, tier re-rank, calibration move, barrier hold and
selection-regressed note each printed EVERY era for EVERY member - ~940
eras/member/day - long after the systems they watch were confirmed
working. Yesterday's file was 1.3GB (70% of it the news-filter calendar
spam the sweep fix already removed).

VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace;
that track is dead since the 2026-08-16 pivot) and gains a second job:
false throttles each settled per-era print to eras 0-3 plus every
TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member);
true restores the per-era firehose, flippable live.

Never throttled: anything that marks a CHANGE - new bests, restores +
eta decays, plateau stage transitions, deploy approvals, warnings,
errors, the label-cache/adoption one-shots, and the combined-vote gate
line (the active system's primary telemetry, still every era).

Semantic fixes over blanket gating:
- barrier hold now ARMS silently and prints only when the hold outlasts
  the 2-min report interval - a brief hold every era is the design, the
  long hold is the watchdog case the line exists for;
- the ensemble deploy REFUSAL prints immediately when its reason
  changes (that is a finding), on cadence when unchanged;
- the filtered-view census prints when its RESULT moves (drawn count,
  or strongest vote by >=2pp) and at least every 10th sweep.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:36:15 -04:00
AnimateDread
83ae56cc9e feat(hud): per-member neuron lines + a vote label that moves as the nets learn
Both 2026-08-19 reports were the same staleness: every source behind the
label was an ERA artifact (live cache refills at pass-3 completion, the
snapshot copies once per era, dPrevSignal is the frozen purge-band edge
bar) - so the readout stepped at era cadence at best, stayed glued to
one direction, and lagged the era counter.

DisplayInference(): throttled (4s, 1s across an era boundary),
SIDE-EFFECT-FREE forward of the current decision bar (window ending on
bar 1, same question the live path asks) through the LEARNER net.
Batch-norm running stats are bracketed frozen/RESTORED via the new
CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore,
not unfreeze, because a display tick can land between pass-3 chunks whose
whole scan holds them frozen. Writes nothing a trading or training path
reads (dPrevSignal, NMS state, tallies, watermarks all untouched;
RefreshLatestSignal is not reusable here precisely because it writes all
of them). LSTM safe by construction: h/c zeroed per forward.

ProspectiveVote() reads the fresh forward as its FIRST source; the
era-artifact chain becomes the fallback (meta head, warm-up, window
holes).

DisplayHudLine(): the reference library's training label, per ensemble
member - name, output activations (softmax probs or raw scalar), the
decision, its weighted vote (the exact consensus numerator term), era,
recent average error, "(trn)" while not vote-capable. Rendered under the
vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines
are telemetry, not tradable readings), coloured by the member's own
direction in muted tones - the vote line's strict
green-only-when-it-would-trade rule is untouched.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
AnimateDread
c85bd9f516 feat(inputs): 5-point vote-threshold steps below 50 - the consensus rungs sit between the old 10s
Under CONSENSUS arithmetic the vote is quantized by agreement: with four
members at pooled tier weights ~29, unanimity reads ~29, 3-of-4 ~22,
2-of-4 ~14.5. The 10-point dropdown straddled every one of those rungs -
20 admitted 3-of-4, 30 admitted nothing - so the thresholds an operator
actually wants (between rungs, e.g. 25 = "unanimity or a top-tier 3-of-4")
did not exist. PERCENTAGE_PRESETS and VOTE_CLOSE_PRESETS both gain
5/15/25/35/45; members are only ever ADDED (explicit values, .set-safe),
never removed - MT5 does not validate saved enum values.

Min_Vote_Open default 40 -> 25: the 40 was priced under union arithmetic
and now sits above the unanimity ceiling (~29), i.e. a fresh attach would
silently never trade - the same fired-on-0-bars defect the 50 -> 40 move
fixed once already.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:25:49 -04:00
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
659638e072 fix(chart): the prospective vote was four models' opinion of ONE frozen bar
"Still glued to buy." Verified in the pass structure rather than guessed:

dPrevSignal is written ONLY by pass 1 (Training.mqh 1439/1442 - the sole
assignment sites), and pass 1 SKIPS the feedForward for any bar a later pass
will forward anyway (laterPassForwards) - which is the whole OOS window and
the calibration band. Pass 3 forwards the newest bars every era but never
writes dPrevSignal. Net effect: after every era, dPrevSignal holds the
model's opinion of the newest PURGE-BAND EDGE BAR pass 1 happened to forward
- one fixed mid-history bar, re-evaluated era after era. The readout was
therefore showing four models' verdict on the same frozen bar, and that bar
reads Buy. Glued to Buy, with flashes of Sell only while pass 1 was actively
walking (the one window where dPrevSignal moves).

ProspectiveVote() now reads the newest ARROW-CACHE entry first (walking back
from the decision bar, bounded at 16), falling back to dPrevSignal only when
the cache holds nothing. Pass 3 writes the adjusted decision for the newest
(OOS) bars each era, so the cache's first non-sentinel entry is the model's
most recent verdict on near-current data - and it is the same value the
filtered overlay draws from, so the label and the reconstruction stay one
quantity. A cached Neutral stops the walk: that is a real decision (vote 0,
abstain -> shows in the "flat" count), not a missing one. Early in an era
the cache is wiped to sentinel and everything falls through to dPrevSignal
exactly as before, until pass 3 refills the newest rows.

Expect the label to change per era now (as each pass 3 re-scores the newest
bars under that era's weights), with the vote/flat split moving as members
genuinely flip between direction and Neutral on recent data.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:47:51 -04:00
AnimateDread
4f2d81a52e fix(chart): sweep hammered the news filter; peak was a fossil; neutrals invisible
Careful read of the 21:14 log window (user report: peak stuck at 50, label
sticky, neutrals never shown). Three distinct defects, one commit because
they share the two files.

1. 15,508 "CalendarValueHistory failed" lines in 68 SECONDS - ~230/second.
The overlay sweep replayed Direction() on EVERY non-AI filter, including the
news/session/risk-guard veto filters. The news filter calls
CalendarValueHistory per evaluation and MT5's calendar cannot answer more
than ~30 days back (the known calendar cliff), so every historical bar
logged a failure - real wall-clock burned inside a sweep whose whole point
is to stay cheap. Veto filters keep m_pattern_count at its 0 default (the
same test UpdateSignalsWeights keys on): they cast no weighted vote, and a
prohibition cannot be reconstructed faithfully anyway - it joins order
validation in the cannot-replay family. Skipped.

Compounding it: at era ~200 the four members complete a barrier round every
~20s while a full 5,000-bar sweep takes ~17s of slices - the sweep finished
and instantly re-armed, forever, against arrow caches half-rebuilt mid-era.
That is why the census's "had a voter" flapped 1299 -> 257 -> 1113 across
three back-to-back sweeps. Re-arms now rate-limited to one per 5 minutes.

2. Peak 50 was a FOSSIL. m_votePeak never reset, so it still held a value
attained under the 25/50/75/100 DEFAULT tier weights from the attach window
before the first re-rank - unreachable ever since the weights became
measured (pooled 27-32 in the same log). A ceiling nothing can reach reads
as "the models are underperforming their own history", which is backwards:
the history was priced in different money. The peak now resets at the same
regime boundary as the census (StartFilteredOverlay), and the label shows
max(live peak, census strongest-vote) - the census number is the actual
answer to "can Min_Vote_Open ever be reached", measured over ~5,000 bars
under the CURRENT weights.

3. Neutrals were invisible. The prospective count lumped Neutral-deciding
models in with voters, so "4 model(s)" read identically whether all four
voted or three sat flat. Now "2 vote/2 flat", and an all-neutral bar reads
"VOTE flat ... 0 vote/4 flat" instead of "--" - the models answered, and
the answer was Neutral.

Expected values, from this log's own re-ranks (all four members' fires land
in T3; tier weights 27-32; module weights 0.27-0.32): a unanimous-buy bar
reads ~29-30%, mixed membership 28-34. The reported "stuck at buy 28,
climbed to 30, flashes of sell, now 33.4" is those weights doing exactly
what they should. The stickiness between moves is pass 2/2.5/3 - only pass
1 writes dPrevSignal, so the label holds the last pass-1 bar's decision for
the remainder of each era. Display-only, and honest: it is the model's most
recent output.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:18:55 -04:00
AnimateDread
b05b4f21d7 fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.

The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.

The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.

Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
AnimateDread
a042cb4359 feat(chart): show the PROSPECTIVE vote while the models are still training
The readout sat at "VOTE 0.0%, 0 voters" constantly. Correct, and useless.

LongCondition()/ShortCondition() return 0 behind the readiness gate for the
entire training run - a model that is not deployed does not vote - so the LIVE
vote is structurally zero for hours, which is exactly the period the readout
is being watched. Worse, it was the same display whether the models were
silent, undeployed, or the filter list was empty: three different situations,
one number.

When no filter casts a real vote, the readout now shows the PROSPECTIVE one -
what these models are saying right now, through the identical tier/weight
arithmetic, minus the readiness gate. That is the same quantity the historical
overlay reconstructs on cached bars, deliberately, so the live line and the
reconstructed arrows are the same measure and can be read against each other.

It can never be mistaken for a decision: labelled "-> training, not tradable
yet", drawn dimmer than "no trade", and `fires` is forced false regardless of
magnitude, because saying "-> TRADE" about a number that cannot place an order
is the precise overstatement this readout exists to prevent. m_direction is
untouched - display only, no trading path reads it.

Confirms the sweep fix from 155f56e is live and working:
  "Filtered view: swept 4999 bar(s), 767 had a voter, drew 0 arrow(s).
   Strongest vote 36.0% against a 40.0% threshold."
4,999 bars against the previous 0. The remaining emptiness is the models, not
the plumbing - see the reply for why lowering the threshold further is the
wrong response to it.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:29:27 -04:00
AnimateDread
8ab8cf6b92 feat(chart): on-chart vote readout, and Min_Vote_Open 50 -> 40
THRESHOLD. 40 is a measured correction, not a preference. Once
RankTiersFromOos() replaced the designed tier priors with each model's real
held-out win rate, the vote converges on that win rate - logged 2026-08-18 as
pooled 23-36% across four members on three symbols - so a 50% bar could not be
reached by anything on offer and the ensemble gate fired on 0 of 4,865 OOS
bars. 40 clears the ~34% break-even those same lines report without being
unreachable. The comment says plainly not to copy the number: break-even is a
function of the barrier geometry, so read the gate's own "needs >N%" for the
config in front of you.

READOUT. One line, top-right:

  VOTE SELL  37.2%  peak  44.1%  need 40%  3 voter(s)  -> no trade

Every other number on the chart is downstream of the weighted mean the open
threshold is compared against, and that was the one quantity never displayed.
A chart with no arrows could mean the models abstained, the vote was diluted,
or the threshold is unreachable - and telling those apart meant waiting for an
era to end and reading the gate line, which is how the last two sessions went.

PEAK is the part that earns its space. A threshold above what the vote ever
attains can never fire, and that is not knowable from a single bar - it is
precisely the "unreachable gate vs merely unmet gate" confusion this project
has paid for twice. Colour carries the verdict rather than the direction:
green/red ONLY when the vote would actually place an order, grey otherwise.
Green-for-buy would make a below-threshold buy look like a trade, which is the
specific misreading the display exists to prevent.

Guarded on `total > 0` for the same reason the normalization is: Direction()
is inherited as-is by every leaf filter, so without it each filter would write
its own opinion into the one shared label and the last to run would win - the
reader would be looking at an arbitrary member's number believing it was the
vote. Drawn after the +-100 range check, so it shows what the threshold is
actually tested against.

CORNER_RIGHT_UPPER: the status lines, control panel and ensemble panel all
live on the left. Registered in WarriorChartPrefixes() explicitly even though
the "Warrior" catch-all already reaches it - that catch-all exists because the
list has drifted twice, not to make entries optional.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:22:45 -04:00
AnimateDread
155f56e0c0 fix(chart): the overlay sweep compared a series index against a bar count
"Filtered view: swept 0 bar(s), 0 had a voter" on charts whose models were
reporting thousands of held-out fires in the same second - the contradiction
the user spotted in the log.

m_overlayIndex is a SERIES index (0 = newest, counting backwards in time), but
its floor was computed as `barsAvail - span`, which is a count from the OLDEST
end. Two different coordinate systems. On XAUUSD's 15,049 bars that produced a
floor of 10,049 against a start of 5,000, so `m_overlayIndex >=
m_overlayStopIndex` was false on the very first test: the sweep reported
completion having touched nothing, and re-armed and "completed" again on every
era boundary. Both bounds are now series indices - start at the oldest bar to
reconstruct, stop at 2 (bar 1 is the decision bar the forward path owns, bar 0
is still forming).

The 150-bar indicator warm-up margin was being applied to the floor, where it
could only ever be wrong; it is a cap on how far BACK the START may reach, on
the same axis. m_overlayOldest is renamed m_overlayStopIndex because with
index 0 = newest that bound is the most RECENT bar, not the oldest - the name
said the opposite of what it held.

Verified across chart sizes: 15,049 bars -> 4,999 swept; 4,865 -> 4,714;
400 -> 249; 301 -> 150.

The census line added in 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
AnimateDread
129a0d448d fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.

Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.

The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.

Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.

AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.

  "0 arrows, best 41.3% vs threshold 50%"  -> a finding about the models
  "0 arrows, 0 bars with a voter"          -> a finding about the plumbing

They need different fixes, and until now the chart said the same thing for
both.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -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
07aa01777c feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.

The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.

THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.

TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.

Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.

SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.

Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -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
9bd7bd1b7f fix(reset): say what the reset actually did, per member and per file
The user reports "Delete & Reset Weights only wipes the first NN". I could not
find a code path that skips ensemble members, and I am not going to assert one:
the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register
unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder
so the six deleted paths are genuinely distinct per member. What IS true is that
the whole success path was SILENT - six FileDelete calls per member printing only
on failure, and one chart-wide Alert - so a four-member reset and a one-member
reset produce byte-identical output. The symptom could be neither confirmed nor
refuted from a log. That is the defect I can fix today.

- COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag
  had sat at scan-nofwd-v5 across a week of commits, so it could not answer the
  question it exists for. The compile stamp cannot be forgotten. Tag bumped to
  reset-census-v6.
- RegistryLine() (public): ID, active file path, common/local, era, deployed vs
  training, ensemble index. The reset handler prints a numbered census of the
  whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart
  the fault is registration, not the reset - and RegisterAISignal already has a
  loud MAX_AI_SIGNALS message for exactly that.
- The confirmation dialog now names the count, so a wrong registry is visible
  before anything is deleted rather than after.
- ResetWeights prints one line per member: N deleted / N already absent / N
  FAILED, plus a per-suffix breakdown. "absent" on a member that should have had
  a .nnw is a completely different fault from "deleted"; they were identical.
- ResetWeights' return value was discarded. A member whose BuildFreshTopology
  fails has had its files deleted and has no network - and the Alert still said
  "weights reset". Counted now, with an INCOMPLETE alert when they disagree.
- Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared
  file for every signal on the chart, so there is nothing per-member to loop -
  the log now says that explicitly, since it is the question being asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00