Commit graph

942 commits

Author SHA1 Message Date
AnimateDread
5ec8902020 fix(gate): the zero-skill floor was still halved one level down - and the measurement sizing actually needs
THREE FINDINGS, ALL FROM READING THE FLEET'S OWN LOG RATHER THAN THE COMPILER.

1. THE MEMBER DEPLOY GATE STILL USED THE 3-CLASS ZERO-SKILL FLOOR.

The ensemble gate's chance rate was corrected on 2026-09-03 when the head stopped
choosing sides. SOosTally::ChancePrecPct - one level down - was not, and it feeds
more than the ensemble copy does: the MEMBER deploy gate, the certified pair
HasDemonstratedEdge() reads to decide whether a member may vote at all, and the
cross-instrument pooled certificate. On a live H1 window it read 12.5% where the
honest always-ride floor is 21.1%, so every member showed "+13pp edge, PASSES"
and was admitted to the vote. A model with no skill whatsoever cleared it.

The same halved floor was in BaselineComparator's Alglib comparison, which scored
the forest and the linear baseline against a bar half the height of the one the
net is judged by.

ChancePrecPct now TAKES THE POLICY AS A PARAMETER WITH NO DEFAULT, so the next
head change is a compile error at every reader instead of a silent wrong answer
at some of them. That is the whole lesson of finding this one a week late: the
first fix was applied where the bug was noticed, not everywhere the assumption
lived.

The era line said "the gate ranks on the LARGER of the two" for that entire week,
on six charts, every era. It now states the policy actually in force.

2. THE ERA LINE PRINTED +/-1.79e308 IN THE MIDDLE OF EVERY RECORD. The binary
head has no third output neuron, so slot 2's min/max kept their DBL_MAX ctor
values and were formatted anyway. Width-aware now, and the slots are labelled
PAYS/DOESNT rather than B/S/N, which is what they hold.

3. THE SIZING QUESTION NEEDED A DIFFERENT MEASUREMENT THAN THE ONE I BUILT.

The reliability curve says the confidence is now HONEST - carried, out-of-sample,
ECE 38pp -> 1.3-2.4pp. It says nothing about whether it RANKS, and ranking is
what a bet size needs. The existing conviction curve cannot answer it either:
coverage collapses above the lowest rung, so every fired call sits in one bucket
and there is no curve to read.

So the ensemble now carries the calibrated confidence per OOS row - the mean over
members that actually called, which is exactly what LiveSignedConfidence()
publishes - and the era verdict splits the certified rung's fired calls at their
median confidence and compares what the two halves earned, in ATR per call.

A MEDIAN SPLIT, NOT A DECILE CURVE, and the reason is power: per-call SD is ~3
ATR and the labels overlap ~34 bars, so a decile of a few hundred raw calls holds
under ten INDEPENDENT ones and its error bar is wider than the whole book. Ten
noisy points would invite the best-of-N reading this project has already crowned
four times. Two halves is the most the data can be asked for, and it is reported
with its standard error on INDEPENDENT counts plus the size of difference this
window could ever resolve - so "not measurable" is distinguishable from "no
effect", which is a statement about the data rather than a verdict on the idea.

Nothing here sizes anything. This is the evidence ConfidenceBridge.mqh's standing
rule demands before anything may.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:50:19 -04:00
AnimateDread
18ada65d85 feat(calibration): the refit column is not evidence - carry the curve and measure the live range
Two corrections to what the first commit reported, both found by reading its own
first live output rather than by the compiler.

THE "AFTER" NUMBERS WERE IN-SAMPLE. ECE 44.7pp -> 0.5pp was the curve scored on
the very band it had just been fitted on, and isotonic regression is expressive
enough to drive that near zero whatever the data says. It is the attainable
floor, not a result. Quoting it would be the calibration-slice error - a
threshold fitted on memorised bars - one layer along.

The fix is free, because the previous era's curve is already sitting there when
the new one is fitted: score it too. That curve was fitted on EARLIER bands and
has never seen these bars, so it is a genuine out-of-sample calibration
measurement. The log now reads raw -> carried -> refit, and says in the line
itself that the middle column is the one to read.

THE SPREAD, NOT THE ERROR, IS THE SIZING QUESTION. The first fit showed claimed
0.75 and claimed 0.99 mapping to the SAME calibrated 0.273 - the curve is flat
at the top, meaning higher confidence there does not mean higher accuracy. A
well-calibrated constant is still a constant: no amount of calibration makes a
flat map rank anything, and a bet scaled by it would be a bet scaled by noise -
the precise error that killed the five confidence-scaled modes on 2026-08-25.

So the report now restricts to the calls the operating point actually ADMITTED -
the only ones that can become a trade - and states the calibrated probability at
the lowest and highest admitted bin, their spread, and the map's mean against the
realised rate on that same set. Carried as its own counts rather than derived by
cutting the histogram at a magnitude, because the operating point is a MARGIN and
this histogram is keyed on a MAGNITUDE; those are monotone in each other on the
binary head and not on the 3-class one, and a reparameterisation guess is exactly
the kind of thing that reads as a measurement.

This is the number the sizing decision will be made on, and it is deliberately
being gathered BEFORE anything is built on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:36:08 -04:00
AnimateDread
92ccf20385 feat(calibration): a scalar has no shape - the isotonic reliability curve
The model's confidence was corrected by ONE number: accuracy divided by mean
claimed confidence, EMA-blended per era. That can move the reliability curve up
or down and can do nothing else. A model that is honest at 0.55 and wildly
over-confident at 0.95 has a SHAPE problem, and no scalar has a shape.

The scaffolding for the fix already existed and was better than expected.
RunCalibrationPass already walks a PURGED, held-out band with batch norm frozen,
visiting each bar exactly once, and fills a 50-bin (margin, hit-rate) histogram.
That is a reliability diagram on out-of-fold predictions. Isotonic regression is
a fit over data already being collected, on a band already purged - no new walk,
no new holdout, no new cost.

WHAT THIS ADDS

  * A SECOND histogram in the same walk, keyed on |dPrevSignal| - the magnitude
    the consumer actually holds - not on the winner-vs-rival margin. Deliberately
    not a re-key of the existing one: on the 3-class head the two statistics are
    not the same quantity, and FitDirConfThreshold's own note records what
    happened the last time one curve served two fits.
  * FitCalibrationCurve: pool-adjacent-violators over the occupied bins, weighted
    by call count. The least-squares monotone fit (Ayer et al. 1955); the
    monotonicity constraint IS the regularisation, so there is no smoothing
    parameter to tune and it cannot fit a shape the data does not show.
  * EMA-blended across eras with the same smoothing the scalar used. A convex
    combination of monotone sequences is monotone, so blending costs nothing the
    fit exists to impose.
  * Brier, ECE and MCE reported before and after the map, on the band the map was
    fitted on. REPORTED, NOT OPTIMISED - nothing selects on them. A model that was
    already calibrated shows all three pairs unchanged, which is the outcome that
    says this map is not needed.
  * Persisted (.stats WSTE), because the curve is produced only by a completed
    calibration band and a DEPLOYED model runs no more eras - the same failure the
    tier ladder had. The bin count leads the block so a future
    DIR_CONF_THRESHOLD_BINS change is a mismatch the reader detects rather than
    fifty doubles landing in the wrong slots.

THE SCALAR STAYS as the unfitted-model answer, and the two are never applied
together: they answer the identical question, the curve per magnitude and the
scalar on average, so stacking them would correct the same error twice. Same
discipline as the logit adjustment being backward-pass only.

NOT RETRAIN-FORCING. The fingerprint is untouched, the .nnw is untouched, and a
WSTD file still loads - it simply comes back with no curve and keeps the scalar.
The fleet resumes exactly where it was.

STILL TELEMETRY. Variables\ConfidenceBridge.mqh carries a standing rule that
nothing there may steer a trade, and CalibratedConfidenceMagnitude has exactly
one consumer: the trade journal's aiConfidence bucket. Five confidence-scaled
trade-management modes were deleted on 2026-08-25 for one stated reason - the
confidence was known to be miscalibrated, so they scaled money by a quantity
whose units were never established. This is the measurement that establishes
them. Sizing is deliberately NOT in this commit: shipped alongside its own
calibration it would be untestable, because if the book moves nothing says which
half did it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:30:51 -04:00
AnimateDread
84789ea992 fix(head): the binary logit branch printed the 3-class banner
ApplyLogitAdjustment's meta-head branch sat BELOW the 3-class tau computation
and its log block. The offsets installed were correct - the branch returns
before the 3-class ones are written - but the 3-class banner printed first and
latched m_logitAdjustLogged, so every era log claimed "APPLIED across all
three" while the head had two logits, and the binary line was unreachable.
A log line that misdescribes a live gate-adjacent path is the same class of
defect as a stale comment.

Moved above the 3-class work. Also corrects this file's own claim about the
imbalance against the measurement it now has: the H1 fleet's shares are
9.4/9.0/81.6, so the binary split is ~4.4:1 rather than the ~3:1 estimated
before the run - and tau goes from 0.57 capped on three classes to ~0.86 on
two, which is the point: most of the correction gets through where most of it
was being clipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:50:05 -04:00
AnimateDread
8d6cff7fcb feat(head): the side was never a prediction - the binary meta-label head
The leg-ride target's SIDE is the direction of the ZigZag leg in progress as
of that bar, readable from bars t and older with no lookahead. The 3-class
head made the network re-derive it anyway: that is Lopez de Prado's PRIMARY
MODEL being learned instead of used, and it cost on four axes at once -
chance at 33% instead of 50%, an 8:1 imbalance instead of 3:1 against a tau
already pinned at its cap, every Buy row spent as evidence about "is this a
long leg" rather than about payoff, and a directional error scored the same
as a payoff error though only one of them was a question we asked.

The head is now binary: slot 0 "riding this leg to the flip pays at least
LEG_LABEL_MIN_RIDE_ATR", slot 1 "it does not". The side comes from
LegDirAsOf() at read time.

TWO OUTPUT NEURONS, NOT ONE, and that is what made this small. Both backward
passes in AI\Impl\NetForward.mqh already carry a `total == 2` softmax+CE arm,
left in deliberately when the old meta head was removed on 2026-08-25 - and a
2-class softmax IS a logistic/BCE head, the logit difference being the
log-odds. So this reuses the exact gradient path the 3-class head uses instead
of growing a second one.

NOTHING DOWNSTREAM CHANGED. ApplyClassificationSoftmax() expands (pGo, side)
back into the [pBuy, pSell, pNeutral] triple every reader already consumes, so
Argmax3, the operating-point histogram, per-class recall, confidence tiers,
the vote currency and the chart arrows are untouched. Neutral stops being a
class the net competes for and becomes what it always meant: pGo below the
operating point. The margin the threshold is expressed in becomes 2*pGo-1,
monotone in pGo, so the calibration walk fits the same statistic.

THE HEAD STAYS BOUNDED, deliberately, and the 2026-07-27 unbinding retry is
NOT bundled here. Reading the gradient showed why it need not be: the softmax
arm OVERWRITES the output neuron's gradient with (target - softmax), so the
sigmoid derivative and MIN_ACTIVATION_DERIVATIVE are already bypassed at the
head. What SIGMOID x CLASS_LOGIT_SCALE actually costs is p in [0.0025,0.9975]
- three orders of magnitude wider than the band where the live question is
0.35 versus 0.60. Unbinding has its own failure history and deserves its own
measurement; batch norm before the head, its precondition, already ships.

THE ZERO-SKILL FLOOR HAD TO MOVE WITH IT. The rate gate's chance was the
better of always-Buy and always-Sell. This head cannot choose a side, so its
no-skill policy is ALWAYS-RIDE - every bar taken in its own leg's direction -
which is right on EVERY directional-label bar, not the better half. Left
alone it would have handed a model with no skill whatsoever a ~+11pp edge.
The book gate already measured against always-ride; the rate gate now agrees
with it about what zero skill means. Same correction in the module-weight
shrinkage prior (Lifecycle.mqh's 50.0 side coin-flip).

RETRAIN-FORCING twice over: |MHEAD:1 joins the fingerprint and the output
count is field 6 of the .nnw filename, so no existing model or pool row can
be adopted. Verified live - all six charts rejected every peer file by
fingerprint and restarted from era 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:46:50 -04:00
AnimateDread
8d7cbdbf9d feat(training): sample the queue by uniqueness instead of weighting it - the update itself was the cost
Weighting by uniqueness (8351c39) redistributed gradient mass but still spent an
update on every redundant label. Lopez de Prado's actual prescription for
redundancy is the other one: bag size = average uniqueness (AFML ch.4). A label
now enters an era's training queue with probability proportional to its own
uniqueness, and carries no uniqueness weight once it is in.

UNIQUE_WEIGHT_VERSION becomes a MODE so the two can never stack - one integer
cannot be two values, which is the same class of mistake as the focal-loss and
logit-adjustment stacking this codebase already removed. 0 off, 1 weight, 2
sample. Mode 2 is the default.

- The draw is STRATIFIED BY CLASS. Uniqueness is correlated with the label here,
  since a label's span IS its ride and whether a ride clears 1 ATR is what
  separates Buy/Sell from Neutral. An unstratified draw would shift the training
  set's class balance away from the priors the logit-adjusted loss corrects
  against - the exact failure the queue site's own comment warns about. Dividing
  by the class mean equalises the expected acceptance rate across classes.
- It also decorrelates the ensemble: each member draws its own subset each era,
  so the four models no longer see one identical stream. That is the diversity
  lever the era report has asked for every era (4 models worth 1.3-1.9).
- Measured: eras now queue 7.4-9.0% of eligible bars, matching each chart's mean
  uniqueness, and era time fell from ~107s to ~22-27s on the index and oil.

One bug shipped and was caught in the first run. The probability was
rate * u / classMean, whose mean inside a class is 1.0 by construction, so at
rate 1.0 it kept 73% of bars and the reduction never happened. The class mean is
the right stratifier but the wrong scale; the scale is the global mean. Fixed
before this commit, and the rate now rides in the fingerprint (UWGT:2:R100)
because a model trained on a twelfth of the bars is not interchangeable with one
trained on all of them.

RETRAIN-FORCING. Compiled 0/0. Deployed 20:15 as uniq-sample-2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:18:40 -04:00
AnimateDread
8351c3933e feat(training): weight every sample by its label's uniqueness - the loss was counting 34x redundancy
Every statistic in this program deflates overlapping labels. The training loop
did not. EffectiveSampleSize() is called in exactly two places, the feature
keep-screen and the deploy gate, and both are reporting paths; the gradient step
weighted each sample by |ride| alone, with no term for how many other labels are
made of the same bars.

Measured on the H1 fleet: mean label overlap 31.6-37.4 bars. USDJPY carries
125,214 labels that this same binary reports as ~3,344 independent ones. The
optimiser was told it had thirty-four times more evidence than it has, which is
the textbook cause of a strong in-sample fit with a thin out-of-sample book -
the pattern every era log has shown since this label shipped.

The correction is Lopez de Prado, Advances in Financial Machine Learning ch.4:
average uniqueness is 1/concurrency averaged over the bars a label spans, and
the prescribed weight is uniqueness x attributed return, i.e. the product of the
new Variables\UniqueWeight.mqh and the existing MoneyWeight.mqh.

- ComputeLabelUniqueness() runs once when the label cache pre-build completes,
  O(bars) via a difference array for concurrency plus a prefix sum over 1/c, so
  a 125k-label cache costs two sweeps rather than millions of span walks.
- On the leg-ride target the two weights are anti-correlated: a label's span IS
  its ride, so a long leg earns a big money weight and sits where concurrency is
  highest. Money weight alone concentrated gradient on exactly the least
  independent evidence in the set. The product is re-clamped to [0.25, 4.00]
  because two clamped factors multiply to a 16x tail.
- Measured even when the knob is off, so the report can show the spread this
  would apply without training on it. First run: mean uniqueness 0.076-0.085.

This REDISTRIBUTES gradient mass; it does not reduce the update count. Training
on fewer, more independent samples is the sequential bootstrap (ch.4 s.4.5) and
gets its own commit and its own measurement.

UNIQUE_WEIGHT_VERSION 0 restores the prior behaviour and the prior fingerprint
exactly. RETRAIN-FORCING (|UWGT:1). Compiled 0/0. Deployed 19:40 as
uniq-weight-1; all six H1 charts from era 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:42:43 -04:00
AnimateDread
3aa15c8b4b feat(altdata): publication stamps by series, alt block back in, one pool for all six charts
Operator asked for the alt data to be properly mapped on H1. Three findings:

1. The as-of join was already whole-day: any H1 bar of day D reads row D, and the
   window layout (ALTW:2) carries that reading once per window at the anchor.
   What was wrong was the ROW DATE. Every FRED series was stamped "knowable next
   day", which is right for a market close and five to six weeks early for a
   monthly print: July CPI (dated 07-01) entered the export on 07-02 and was
   released 08-12. Unemployment the same; the H.10 dollar index (weekly, posted
   the following Monday) a week early; the effective funds rate a day early. On
   H1 that is ~1,000 bars of a value nobody had, in three of the twelve fleet
   columns. FredPublishLagDays() stamps by series (CPI +48d, UNRATE +40d,
   DTWEXBGS +8d, DFF +2d, closes +1d), cached rows are re-stamped on load, and
   ALTFETCH_EXPORT_VERSION (a .ver sidecar beside each export) forces one rebuild
   at the next init so every chart reads the corrected export immediately.
   Verified on XAUUSD_D1.csv: mac_cpi now changes on 08-18, mac_unemp on 08-10.

2. The alt block was not reaching the model at all. Keep mask v2 dropped all
   twelve alt columns on a screen measured under the pivot label on H4, and the
   screen only reports on emitted columns. v3 emits them again (28 of 47 columns,
   input width 168); the H1 keep-screen will say which of them clear.
   The window dedupe now uses the EMITTED alt width, not the panel's: under v2
   it placed a 12-wide block over the last twelve of sixteen emitted columns.

3. The training pool ran as two groups because the cross-asset block encoded
   index mode (base == quote: SP500, and this broker's XAUUSD/XTIUSD) with a
   different meaning per slot than FX mode, and the fingerprint tagged it
   ":IDX2". U3 gives both modes one layout (proxy fast/slow in 0/2, denomination
   fast/slow in 1/3, own move minus the proxy-vs-denomination cross in 4), so all
   six charts print the same fingerprint and pool together.

RETRAIN-FORCING (XA:6:U3, ALTV:2, FMASK:3). Deployed 15:00 as alt-stamps-1; all
six H1 charts from era 0, one shared fingerprint, exports rebuilt at v2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:02:27 -04:00
AnimateDread
8c37869c63 fix(init): a warm re-init kept the H4 run's globals; the training pool adopted other timeframes
Operator report 2026-09-02: the six charts were switched from H4 to H1 and the
status label kept showing the H4 run. MetaTrader does not unload the program on a
timeframe, symbol or input change - it runs OnDeinit and OnInit inside the same
instance and every file-scope global survives the pair. The members' converged
flags, the live vote line, the panel rows, the shared best-era/plateau state and
the once-per-chart report flags all belonged to the models just torn down.

- Warrior_EA.mq5: WarriorResetWarmReinitState() runs first in OnInit and puts
  every such global back to its cold-start default. Kept on purpose: the chart's
  book magic (positions opened before the switch stay owned), the alt-data fetch
  throttle (rate-limited APIs), the tester profile, the RNG, the OpenCL flags.
- TrainingPool.mqh: peers must be the caller's own timeframe. The fingerprint
  does not carry the period, so the first H1 census adopted 39,754 H4 rows from
  three peer files and credited them to the capacity budget. Files are named
  SYMBOL_PERIOD.bin; the suffix decides, and the reader names the rejection.

Build tag warm-reinit-1. Compiled 0 errors / 0 warnings. RETRAIN-NEUTRAL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 14:25:20 -04:00
AnimateDread
980f10b60c feat(label): ride the leg, exit on the flip - the leg-ride target replaces the pivot-event target
The pivot label paid +1.6 ATR on a hit and -1.7 on a miss at 50-65% precision:
a zero book by arithmetic, because a "bottom" that is not one is a move that
kept going, and the model called it because a big move had just happened.
This target never asks for a turn. Direction is the leg in progress, known on
the bar; the label is whether riding it from here until the leg flips pays at
least LEG_LABEL_MIN_RIDE_ATR (1.0). The flip is the exit the EA now places.

- Labeling/LegState.mqh: a line-for-line replica of ZigZag.mq5 (12/5/3) run
  over bars <= t only, so "which leg am I in" is what the chart showed on t,
  never the final buffer. Verified against the real indicator at every init
  (LEG STATE REPLICA line) and by Tests/Test_LegState.mq5 on hand-built bars.
- LegRideLabel (Labels.mqh): ride = legDir * (close[flip] - close[t]) / ATR,
  the flip found by asking the same as-of function of each newer bar in turn.
  Unresolved until the leg has flipped inside loaded history. Lifespan = the
  leg, so EffectiveSampleSize deflates honestly (~3x fewer than the window
  constant claimed); the topology's overlap is the median leg again.
- Money weight = |ride|. Online step's exit bar = the flip.
- Three as-of leg features in the swing block (direction, extension in ATR,
  age); FMASK:2 keeps them. Fingerprint TGT:LEG1:10 - RETRAIN-FORCING.
- Era verdict: the book WarriorRungBookProfitable gates is the RIDE (entry at
  the call, exit at the flip), printed per rung and at the certified rung
  beside ALWAYS-RIDE (zero skill) and the ORACLE RIDE (the ceiling). Fixed-
  horizon payoff stays as the signal diagnostic. The by-distance profile,
  its slot arithmetic and the reversal-exit pass are deleted (measured:
  the vote's reversals land 33-137 bars late; dead).
- Live: Exit_On_Leg_Flip (default on, not in the fingerprint) closes when
  the as-of leg flips against the position and places no take-profit; the
  measured stop stays. Build tag leg-ride-1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:02:47 -04:00
AnimateDread
635e4396ab feat(training): measure the reversal-exit book - what each call earns held until the vote flips
A perfect pivot caller buys the bottom and sells the top and collects the whole leg: median 4.5-5.0
ATR on these charts (SWING LEG SIZE line at init). The era report's fixed 18-bar exit collects ~35%
of that and a mean-excursion target about half, so the exit - not the label - is where the leg is
left. Nothing measured the one exit that can keep it.

Every era now walks the certified rung's fired calls in time order and scores each one held until
the vote next fires the other way - no stop, no target - in ATR at entry, by side, with the mean hold
in bars, beside the fixed-horizon exit on the SAME calls. Calls with no reversal before the window
ends are dropped like the leading edge; same-side re-fires are ignored. Two per-row fields (close,
ATR) join the OOS vote store to make an arbitrary row-to-row payoff measurable. Reported only, gates
nothing, retrain-neutral.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 10:11:58 -04:00
AnimateDread
39e4bb587a refactor(signal): delete the barrier sweep, the automatic direction policy and the pivot-geometry features
Three layers out, ~1,000 lines, RETRAIN-NEUTRAL - the fingerprint and the emitted feature vector are
byte-identical, and a WSTC .stats still restores under the new WSTD reader.

BARRIER SWEEP. The 64-cell (stop, target) grid, its 999-draw Westfall-Young permutation null, the
first-touch offsets recorded per OOS row, the swept-pair persistence (WSTA) and the tier-1 branch of
ResolveBarrierMultiplier are gone. It never cleared its own null on any chart (p 0.19 to 1.00), so it
gated nothing and cost a grid plus 999 rescans per era. Live stops and targets come from the mean
excursions at the hold horizon, as they did in practice.

DIRECTION IS THE INPUT. WarriorEffectiveDirection() returns tradingdirection. The bar-body drift
screen (Signal\DriftScreen.mqh) and the 2-SE by-side expectancy test that outranked it are removed
with their unfiltered accumulator and persistence (WSTB/WSTC). On a fleet where no side clears 2 SE
on payoff the test could never speak - SP500 and XAUUSD never once printed its verdict across two
days of logs - so the screen decided by default. The era verdict now certifies BOTH sides whatever
the input says: the checkpoint no longer depends on a per-chart input, so one trained model serves
LONG_ONLY, SHORT_ONLY and BOTH and the input can be optimized in the tester without a retrain. The
blocked side's vote still closes a position under Exit_On_Reversal_Vote.

PIVOT GEOMETRY. The five confirmed-pivot swing features (leg direction, distance since pivot,
prior-leg magnitude, retracement ratio, bars since pivot) scored 4/2/0/2/1 of 24 on the keep-screen
and the mask had already dropped them from every emitted vector. Deleted from the builder; the kept
trend-position columns are swing[0..3] now and the vector is unchanged.

SWING LEG SIZE, measured. MeasureSwingGeometry now prints the leg-size distribution in ATR at the
leg's start pivot. Offline on the archived H4 feeds (stock ZigZag 12/5/3, ATR 20): median leg 4.5-5.0
ATR, p90 9-10, max 35-76; median length 13-14 bars, a third of legs outlast the 18-bar hold. The era
report's ORACLE (+1.6 ATR) is a fixed-horizon close-to-close capture from the call bar, not the leg -
a perfect caller entering at the pivot and holding 18 bars earns ~1.9-2.0 ATR on the same feeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 09:55:44 -04:00
AnimateDread
c0a13a7715 fix(altdata): one chart per terminal fetches; the rest subscribe
Every chart runs its own CAltDataFetch with its own m_lastAttempt, so six
charts each believed they were the only one: six identical requests for one
shared cache file left within ~270 ms of each other (measured 2026-09-01,
12:40:15.831 -> 16.097), and six writers raced on the same file.

THIS DOES NOT FIX THE EIA EDGE BLOCK, and the burst theory that motivated
looking here is DISPROVEN. Six concurrent requests from this machine return
200, twice over. So does every request shape MT5 could plausibly send
(duplicate User-Agent, no User-Agent, Content-Type/Content-Length on a GET,
HTTP/1.0, keep-alive), every user-agent string, both resolved IPs, and the
Schannel stack. There is no proxy at the MT5, WinINet or WinHTTP layer. The
403 carries Server: awselb/2.0 with an empty WWW-Authenticate and an HTML
body rather than EIA's JSON error, so it is blocked at the edge before the
API ever sees it - seven mechanisms ruled out, cause still unknown.

What is committed here stands on its own: six redundant requests and a
six-way write race on one file are worth removing regardless.

- ClaimFleetFetchLease: terminal-wide lease via GlobalVariableSetOnCondition,
  a real atomic compare-and-set, so of six charts arriving in the same
  millisecond exactly one wins. A get-then-set would let all six read stale
  and all six proceed - the same bug, just faster.
- KEYED ON THE CACHE FILE, NOT THE THROTTLE SLOT. Slots 2 (COT) and 4
  (implied vol) are per-symbol - SP500 reads raw_COT_SP500.csv where EURUSD
  reads raw_COT_EURUSD.csv. A slot-keyed lease would have let one chart
  suppress another chart's fetch of a completely different file, leaving it
  waiting an hour for data nobody would download for it.
- A LEASE ALONE WOULD HAVE BEEN A DATA REGRESSION. AltRawLoad only ever ran
  on rows == 0, so a chart that skipped its fetch would serve startup data
  for as long as the terminal stayed up. PublishFleetRefresh bumps a
  generation counter only where a fetch actually persisted rows, and
  EnsureSeriesLoaded reloads from disk when another chart has published
  since. Global keyed by file (shared); local memory indexed by slot (within
  one chart a slot maps to exactly one file).
- The attempt stamp is written whether or not the lease is won, so a loser
  does not re-enter every tick to lose the race again.
- Leases are temporary globals: one persisted across a restart would
  suppress the first fetch after every launch.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 08:21:22 -04:00
AnimateDread
3422f1b8f4 fix(barriers): Sidak was inert on this grid - permutation null instead
Sidak's correction is exact only under INDEPENDENCE. The 64 barrier cells
are built from the same first-touch offsets on the same rows: stop 2.5 /
target 3.5 and stop 2.5 / target 4.0 are almost the same trade. Correcting
64 heavily overlapping tests as 64 independent ones does not merely
over-penalise, it removes the test's power entirely.

Measured, 200 synthetic trials at 400 rows with the correlation structure
of real excursions:

  true null       permutation mean p 0.496 (uniform -> 0.500)
                  P(p<0.05) 0.030 vs nominal 0.050 - calibrated, and
                  erring conservative
  edge 0.10       permutation rejects 82%, SIDAK 0%
  edge 0.20       permutation 100%, SIDAK 0%

Sidak's mean p on this family is 1.000. It would have refused a real
barrier edge of any size we could plausibly have had - which is what was
rejecting SP500 at p=0.0519, the one chart in the fleet with positive
excess over its own zero-skill book, whose swept pair is worth +0.426 ATR
per call against a mean-excursion fallback (stop 2.60 / target 2.58) that
is a coin flip by construction since MFE ~ MAE fleet-wide.

- BarrierPermutationP: Westfall-Young maxT. Each draw flips every row's
  DIRECTION on a fair coin and rescores the whole grid; p = share of draws
  whose maximum z beats the observed maximum. Direction is what the claim
  is about, so direction is what gets permuted - the touch offsets, the
  volatility clustering they encode, the row population and the grid are
  all held fixed.
- A sign flip SWAPS which barrier is target and which is stop; it does not
  negate the outcome, because the tie rule and the still-open case are both
  asymmetric. Both direction vectors are therefore built through the same
  shared WarriorBarrierCellOutcome, extracted here so the rule exists once -
  a null resolving ties even slightly differently would be testing a
  different quantity than the one it judges.
- The null's row gate is verified against the grid's own row count at
  runtime and REFUSES (falling back to Sidak) if they differ. That
  divergence is silent by nature: both halves look correct alone and only
  the p is wrong.
- 999 draws. Monte-Carlo SE at p~0.05 is 0.0069 and is PRINTED with the p;
  a verdict within 2 SE of the cutoff is labelled a coin flip in the log
  rather than reported as a finding.
- Both direction vectors are precomputed once per era, so a draw is 64
  additions from a flat array rather than 64 touch lookups plus a call.
  That is what makes 999 draws affordable inside an era report.
- Falls back to Sidak, never to "no correction", if the null cannot be
  built or an unload interrupts it.
- The deploy gate's own best-of-N test is untouched.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 00:05:00 -04:00
AnimateDread
6f53809713 fix(barriers): the swept pair was fitted to a rung the era did not certify
The 64-cell grid accumulated under `if(s == derivedIdxPrev ...)` - the rung
the LIVE signal is currently trading - because this era's derivedIdx is not
known until the row loop has finished. But the pair that wins that grid is
persisted to .stats and becomes the stop and target the chart actually
trades, and it was reported beside a payoff and precision measured at
derivedIdx. Those are different rungs whenever the operating point moves,
which is measured at 6-34% of eras.

The only symptom was an n printed beside a different n, which is why it
survived review: EURUSD reported a swept n of 2048 against 1470 fired,
USDJPY 1767 against 1208. The other four charts happened to agree that era
and looked correct.

- barSum/barSumSq/barN widened to [ENS_THRESHOLD_SWEEP_N * 64], accumulated
  at EVERY rung, read out at derivedIdx once it exists.
- NOT a best-of-384. Every rung is accumulated but exactly one is scored;
  the other five are discarded unread. Nothing selects across rungs, so the
  family the p-value must cover is still the 64 cells of one grid.
- The distance buckets STAY at derivedIdxPrev. They are a report - nothing
  reads them back - so the rung the operator is currently trading is the
  right one for them. Only the barrier grid feeds a persisted decision.
- Cost is integer compares on first-touch offsets already recorded by the
  payoff walk; no prices are read.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 23:53:36 -04:00
AnimateDread
d82e7b8ed5 feat(training): money-weighted loss - cross-entropy could not see magnitude
The objective was never aligned with the book. Cross-entropy against the
pivot label scores a 0.2 ATR pivot and a 3 ATR pivot identically, so the
network optimises how OFTEN it is right and nothing tells it that being
wrong on the big ones is what kills the P&L. That is exactly the measured
pathology: precision 50.4-65.2% against a 13.4-14.5% base rate (23-52
sigma, unambiguous) while the book earns +0.002 to +0.045 ATR per call and
loses to always-long on five of six charts.

The label is NOT the problem. The oracle - a perfect caller of this exact
label - earns +1.167 ATR @5 and +1.627 @19 over 3,986 calls. The target is
rich and we capture ~2% of it.

Per-sample weight = |forward return at the hold horizon| / mean(|r|),
clamped to [0.25, 4.00]. Normalised by the mean, so an era trains on the
same TOTAL gradient mass as before - redistributed from cheap bars to
expensive ones - and the effective learning rate does not move.

- Variables/MoneyWeight.mqh: the knob (MONEY_WEIGHT_VERSION 0 restores
  unweighted training and the pre-change fingerprint exactly), the
  transform, the fingerprint tag. Runtime predicate, not #if - MQL5 has no
  #if <expression>.
- The floor is load-bearing: sampleWeight = 0 IS NOT A SKIP in this
  optimiser (Adam momentum and weight decay still apply, t and
  m_batchCount still advance), so weights must never decay toward zero.
- Cached as the RAW |r| under the label's own validity flag, normalised at
  use: the mean keeps moving as bars resolve, so a cached weight would be
  stale for every bar but the last.
- Peer pool rows stay at 1.0 - the pool record carries no forward return
  and deliberately does not name its source bar. Normalisation is what
  keeps the local:peer gradient ratio unchanged.
- Not lookahead: measured from future bars exactly like the label, reaches
  the loss only, and has no route into the feature vector.
- RETRAIN-FORCING by design. A weighted and an unweighted model have
  identical topology and identical weight-file shape and differ only in
  what they were taught to value; the fingerprint is the only thing that
  can tell them apart.
- Reports the realised weight distribution once per cache, including the
  share pinned at each clamp - a saturated scheme is invisible in every
  downstream number.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:32:12 -04:00
AnimateDread
4ae4cda6b7 fix(altdata): print who sent the non-200 - the 401's body and headers were discarded
The 2026-09-01 fleet-wide EIA 401s could not be diagnosed from the journal:
EIA answers 403 to ANY key problem (including no key at all), the stored key
fetched 200 from the same machine over three TLS stacks and both resolved IPs,
and the one thing that names the refusing server - the response itself - was
thrown away at the non-200 branch.

- non-200 now prints the Server and WWW-Authenticate headers (RFC 7235 requires
  the latter on a genuine 401 - it names the party demanding credentials) plus
  the first 200 chars of the body.
- MaskUrl prints '(EMPTY)' when there is nothing between 'api_key=' and '&';
  the unconditional '***' made a blank key indistinguishable from a present one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 15:01:14 -04:00
AnimateDread
724f8dce83 feat(features): fleet keep mask - 13 of 49 columns, the false-pivot lever
RETRAIN-FORCING. Re-keys every fingerprint (column count 49 -> 13, plus
an explicit |FMASK: token).

Decomposing the deployed fleet's payoff by distance-to-pivot says the
whole edge is one number. A hit pays +0.94 to +1.57 ATR and is FLAT
across d=-3..0; a miss costs -0.76 to -1.62; the miss rate is 34.5% to
49.4%. Remove the misses and every chart earns +0.97 to +1.22 instead of
+0.05 to +0.32. One percentage point off that rate is worth +0.011 to
+0.018 ATR per call.

Two obvious levers are already spent, and the logs say so. Raising the
vote threshold buys precision and hands the expectancy back - SP500's
precision climbs 38.9% -> 57.3% across the six rungs while its book
expectancy FALLS +0.39 -> +0.21 - and the rung search already takes the
best rung available. Exits do not rescue it either: the 64-cell sweep
fails Sidak on all six, p 0.19 to 1.00. Narrowing the label to drop d=+1
was checked and rejected: it is negative on SP500/USDCAD/XTIUSD, neutral
on XAUUSD/USDJPY and strongly POSITIVE on EURUSD (+0.769, n=116).

What is left is discrimination at fixed coverage, and 294 inputs against
~4,700 independent observations is 16 per first-layer weight - the
overfit regime, where an out-of-sample overfit IS a false pivot.

The keep-screen already measured which columns carry information, and
the open question blocking a prune was whether the masks agree. They do.
Intersected across all 24 members - six charts x four architectures -
swing[5..8] and ma[0..1] are UNANIMOUS, ma[2..4] carry 23/23/20 votes,
and 16 of 49 columns are kept by nobody. This mask is every column kept
by at least half the fleet: swing[5..8], ma[0..4], crossasset[3,4],
volume[1,3]. Width 294 -> 78, first-layer budget 16 -> ~60.

Two of the votes are worth reading twice. swing[0..4] - the five
CONFIRMED-PIVOT features - scored 4, 2, 0, 2 and 1 of 24: the label is a
ZigZag pivot event and the ZigZag geometry carries almost nothing, while
trend position carries everything. And alt[0..11] scored 4, 1, 1, 1 with
atr at zero, so a whole rate-limited external pipeline buys nothing
under this label. Alt stays ENABLED and merely unmasked, so the screen
keeps reporting and the finding stays falsifiable.

Implementation keeps one authority for block order and width:
CFeatureBuilder::FeatureBlockTable, which FeatureSlotName, the kept
count and the emit-time compaction all read. The mask is stated
BLOCK-RELATIVE so toggling a block cannot silently re-point it, and
applied once per bar at the existing sanitize seam rather than inside
nine emitting blocks. m_neuronsCount is set from the counted kept
columns, never from a hand-written subtraction, and a one-shot width
check fires if the two ever disagree.

FEATURE_MASK_VERSION 0 restores the full set exactly, including the
pre-mask fingerprint. This is a measurement, not a conclusion: the
screen tests each column's MARGINAL information and cannot see a column
that is useless alone and useful in combination, so judge the retrain
against the per-chart false-pivot rate and book expectancy above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:12:39 -04:00
AnimateDread
5a7d96c948 fix(gate): certify the BOOK, report the split - the per-side test was selecting
WarriorRungBothSidesProfitable required each side's mean ATR/call to be
> 0.0. Per-call returns at the hold horizon have SD ~3.1 ATR - backed
out of the direction policy's own printed band - so a side's standard
error at a few hundred independent calls is 0.24-0.35 ATR. The test
compared that against exactly zero.

It did not merely fail to detect. The derived rung is chosen from six,
and on three charts the short side came out substantially negative at
five of them and a hair above zero at precisely the one that certified:

  USDCAD eS: -0.23 -0.18 -0.13 -0.13 [+0.01] -0.18
  USDJPY eS: -0.27 -0.22 -0.26 -0.23 [+0.02] -0.03
  XAUUSD eS: -0.44 -0.39 -0.28 [+0.02] -0.08 -0.68

A short side does not pay at 25% and lose 0.13 to 0.23 either side of
it. That is a best-of-six finding the cell where noise crossed zero,
with no family-wise correction anywhere on this test - Sidak guards the
precision test only. XAUUSD is the extreme: 337 consecutive eras refused
on this clause, one era passed on eS=+0.02, it deployed 39 seconds
later, and era 339 failed again.

The gate is now the BOOK: pooled expectancy over both sides at the
certified rung. One number, the largest sample available, and what the
account is actually paid. It still catches what the clause was added for
- a side losing enough to matter drags the total under by itself.

AND IT IS A FLOOR, NOT A TEST. Book expectancy is no more significant
than the sides were; nothing in this fleet clears 2 SE on payoff, where
precision clears at 23-47. Demanding significance would deploy nothing,
ever - the prove-your-edge trap this codebase has backed off twice.
Refusing a measured-negative book needs no power, only a sign that is
already the best estimate available. The split stays measured and
printed beside it as a sizing and direction input, never a gate - same
doctrine as alpha, the barrier sweep and the topology floor.

Checked against the 2026-09-01 fleet: all six still deploy and every
derived rung is unchanged. The higher rungs that newly clear the book
test fail the coverage floor instead (XAUUSD 25% at 4.0% vs 6.7%,
USDJPY 30% at 4.4% vs 6.6%, EURUSD 30% at 4.7% vs 6.7%).

Report: SIDE-FAIL -> BOOK-FAIL, and the threshold sweep now prints
book|L/S per rung so the selection pattern above is legible at a glance
instead of needing six eras side by side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 10:26:11 -04:00
AnimateDread
9b69ef09a0 fix(direction): the by-side policy was a sign test on overlapping counts
WarriorMeasuredSideDirection() decided which half of the book to switch
off with `mean > 0.0` per side, gated only on n >= 100 RAW calls. No
error bar, and no deflation for label overlap - in a codebase where
every other verdict carries an exact binomial or a Sidak correction.

What it did live, on one chart, inside fifteen minutes:

  19:35:41 EURUSD long -0.038 (n=591)  short +0.011 (n=559)  -> SHORT ONLY
  19:50:45 EURUSD long +0.063 (n=3687) short -0.046 (n=3238) -> LONG ONLY

The whole book inverted on five hundredths of an ATR. And those n count
overlapping labels at a 5.0-bar mean lifespan, so the honest sample was
about a fifth of the one being tested.

Both sides now accumulate a sum of squares, so each mean carries a
spread. The bar is on the EFFECTIVE count (EffectiveSampleSize, applied
in Training.mqh where the member that knows this model's overlap lives -
the resolver is a free function on the parent header with no object to
ask), and a side is given up only when BOTH ends hold at
DIRECTION_SIDE_MIN_SIGMAS = 2 SE: the side being dropped loses beyond
the band AND the side being kept pays beyond it. "Loses" is the same
bound on the negated mean, one expression, so the halves cannot drift.
An interval straddling zero answers false to both - the state the sign
test could not express, and exactly where EURUSD was when it flipped.
Two sigma, not one, per the operating-point note that a 1-SE band is not
enough when the quantity is noise-dominated.

The spread and the independent count are persisted as WSTC. A WSTB
record restores the means for reporting with a 0.0 spread, which the
resolver reads as "no interval" and resolves to BOTH SIDES until an era
re-measures - the right degradation, since an older file's verdict came
from a test this build no longer trusts.

None of this says the current verdicts are wrong: at 20:00 SP500 was
one-sided on the drift screen (+4.09% of the average bar) and XAUUSD,
USDCAD, USDJPY and EURUSD on the measurement, with XTIUSD on BOTH
because both its sides lose. It says four of them were ACTED ON hours
before the evidence supported them, and one of them acted on noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:25 -04:00
AnimateDread
c4fd0be7c6 fix(topology): the capacity re-derive tested one of three derived sizes
The one-shot re-derive in AutoTune.mqh is the unattended answer to the
cold-start floor, and it works: it fired eight times on 2026-08-31 at
era 0-1, moving the first layer on PAI/CONV across four charts. Timing
was never the constraint - the first pool file appeared at 13:40:42 and
all six had published by 13:41:46, about ninety seconds after sizing.
(The "sixteen minutes" in the old comment was a republish mtime, not a
first publish; corrected here.)

It asked only ComputeFirstLayerWidth(). SP500's and XTIUSD's LSTM and
ConvLSTM had an UNCHANGED first layer and a HALVED LSTM hidden size - 8
where the same charts carried 16 that morning - so the test read "grown
but the width is unchanged", latched m_capacityResizeDone and closed the
door on the one dimension that had moved. Those six models carried a
training-alone topology for the rest of their lives, and they are
exactly the ones yesterday's init-time guard then found and destroyed.

Now it measures all three, in the dependency order the ladders read each
other in (conv -> LSTM -> width; ComputeLstmHiddenSize reads
ConvFilterCount, ComputeFirstLayerWidth reads both), through the members
because that is the only way those reads see a candidate. They are
restored immediately - ResetWeights() remains the only thing that
commits a shape, and it re-measures them itself.

And it counts only the stages this model has. A Perceptron has no LSTM
block and no conv stage; rebuilding one because an LSTM ladder step
moved is a retrain bought for nothing. The log line names whichever
dimension moved instead of always saying "first layer".

Topology.mqh's init-time report is now documented as what it is: the
last resort, not the mechanism. Reaching it means a model outlived the
era-8 window still floored, which is a bug in this block rather than a
case for deleting a trained model's weights.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 18:00:12 -04:00
AnimateDread
2f17801d6b fix(persistence,topology): the .stats magic sorted below WST1, and the resize guard believed it
WSTA/WSTB were typed 0x57535341/0x57535342 - third byte 0x53 ('S') where
the format needs 0x54 ('T'). They spell "WSSA"/"WSSB" and sort BELOW
WST1, so every `magic >= WST_MAGIC_n` test in LoadModelStats went false
and a WSTB file read back as a bare WST1: no tier ladder, no ensemble
record, no g_ensDeployApproved, no certified precision pair, no barrier
widths, no swept pair, no direction evidence.

Measured 2026-08-31. The 13:37 build wrote WST9 and the 15:35 restart
restored the ensemble record on all six charts; the 15:31 build wrote
WSTA-mistyped and at 16:41 not one chart could. SP500 (deployed 14:47,
54.8% vs 14.2% on 374 fired bars, 22.5 sigma) and XTIUSD (14:55, 47.2%
vs 13.9% on 897, 28.8 sigma) resumed with the deploy flag reading false
and every member mute for want of a ladder.

The mistyped files are ACCEPTED, not rejected. Only the label is wrong;
the fields behind it were written correctly and in order, and rejecting
them - a mismatch is treated as absent - destroys exactly the state this
sidecar exists to protect. They are normalised on read; writes use the
correct value. Every magic is a named constant now, because a hex
literal does not show you its own ordering and the ordering is what the
">=" chain rides on.

AND THE RESIZE GUARD ASKED THAT SAME FLAG. 1a14afb rebuilt a model whose
.cfg pinned a smaller topology than today's census would derive, keeping
weights only if deployed or tier-ranked. With the flag unreadable that
arm never fired: twelve of twenty-four models were deleted from eras
78-106 back to era 0, three of four members on each of the two deployed
charts. Two further faults make the automatic rebuild wrong regardless:

  1. It cannot converge. EstimatedInSampleBars() counts the training
     pool, the pool grows every time a chart publishes another era, and
     both ladders snap on that number - so training the fleet enlarges
     the derived topology, which trips the test, which restarts
     training. A rule whose own success re-arms it is a livelock.
  2. It was dimension-blind. A Perceptron has no LSTM block and no conv
     stage; SP500's and XTIUSD's PAI were charged with "LSTM 16 vs 8"
     while their first layer sat unchanged at 16.

So the floor is REPORTED and the weights are kept - resizing a trained
model is a retrain, and that is the operator's call. The no-weights
re-derive above it stands: that case costs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:04:38 -04:00
AnimateDread
636ec4c078 feat(direction,chart): decide the side on measured expectancy; draw the exit vote
TWO DEFECTS, BOTH VISIBLE IN THE 2026-08-31 15:35-15:41 LOGS.

1. THE DIRECTION POLICY GATED ON A PROXY WHILE THE MEASUREMENT SAT BESIDE IT.

The deploy gate refuses a chart whose one side loses and its refusal text says
the remedy out loud - "switching the losing side off with tradingdirection
raises expectancy arithmetically ... the drift screen does that automatically
when the bias is large enough". It did not fire. Measured today:

  XAUUSD  long +0.228  short -0.522   drift +1.36%   -> screen says BOTH
  USDCAD  long +0.105  short -0.264   drift +1.91%   -> screen says BOTH
  EURUSD  long -0.050  short -0.093   drift +0.90%   -> screen says BOTH

Three charts blocked from certifying as one-sided books by a body-drift proxy,
while the direct by-side expectancy - measured on the actual calls, at the
certified rung, at the hold horizon - said the short side loses on every call.
WarriorEffectiveDirection() now reads the measurement first and falls back to
the drift screen only where no era has scored yet. An explicit tradingdirection
still outranks both.

THE MEASUREMENT IT READS IS UNFILTERED, AND THAT IS THE WHOLE DESIGN. The
certified by-side numbers are accumulated BELOW the direction filter, so
feeding those back into the policy would be a ratchet: block the short side
once, score zero short rows from then on, and no evidence could ever re-open
it. A second, unfiltered pair is accumulated ABOVE the filter and answers the
question the policy actually asks - would the blocked side pay if allowed.
Everything below the filter is untouched, so what the gate certifies is still
exactly what the EA trades.

Both sides losing resolves to BOTH, deliberately. That is a bad model, not a
one-sided one; blocking a side there creates no edge and only hides half the
failure from the gate. Both sides need 100 calls before either may move the
policy - a thin side's mean is noise, and noise must not switch off half a book.
Persisted as WSTB: a deployed chart runs no further eras, so without it a
restart would drop it back to the proxy answer this replaces.

2. THE EXIT VOTE WAS INVISIBLE.

Arrows are drawn in CheckOpenLong/CheckOpenShort. On a one-sided chart the
blocked side returns at the direction filter before reaching that code, so with
Exit_On_Reversal_Vote on, the vote that CLOSED the position drew nothing - the
chart showed an entry, then a position gone, and nothing in between. Trading on
a signal the chart cannot show is the certified!=traded defect one layer up.

CheckClosePosition now draws an EXIT mark on its success branch, in its own
SIG_EXIT_PREFIX namespace - not SIG_VOTE_PREFIX, whose sidecar and cooldown
reconciliation would adopt an exit as an entry. Registered in
WarriorChartPrefixes() so a purge reaches it: it lives under SIG_ARROW_PREFIX,
which skipArrows protects. Colour is the ACTION, so closing a long draws in the
sell colour.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 16:20:04 -04:00
AnimateDread
2c2cdfed6a feat(exits): sweep the barrier grid on held-out rows; remove SL_Mode/TP_Mode
PHASE 1 of doing the trade-management search inside the EA instead of in the
MT5 optimizer, for the one pair that cannot wait: the stop and the target.

WHY NOT ALGLIB. The space is discrete - 8 stop widths x 8 target widths = 64
cells. At that size you do not search, you ENUMERATE. Exhaustive has no seed,
no convergence question and no tuning of its own, and it answers the thing a
search cannot: whether the good region is a broad plateau or one lucky cell.

WHERE IT RUNS. Inside the era report's existing OOS threshold sweep - same
held-out rows, same purge, same certified rung the deploy gate uses. No tester,
no agents, no .set.

ORDERING IS THE WHOLE POINT. MFE and MAE cannot say which barrier a trade hit
FIRST, and "both were touched" is the common case, so a grid evaluated from the
extremes alone would be guesswork dressed as measurement. MeasureBarPayoff()
already walks the forward window bar by bar; it now records the first-touch bar
offset for each grid level in each direction. One compare per level per bar on
a loop that already runs. The sweep then costs 64 integer compares per fired
row and reads no prices at all.

Same-bar ties resolve to the STOP. Bar data cannot order two touches inside one
bar, and assuming the target would be the optimistic half of an unknowable coin
flip, on exactly the half that flatters the result.

JUDGED AGAINST THE NULL OF THE MAXIMUM, NOT ZERO. Picking the best of 64 cells
and reporting its own z is the best-of-N error this project has already made
four times, including on the deploy decision. SidakFamilyP over the cells
actually scored is the same correction the rung sweep and the baseline
comparator use. A cell needs BARRIER_MIN_CALLS before it may win at all - a
thin cell tops the grid on noise alone. The pair is STORED either way: a reader
must be able to tell "swept and rejected" from "never swept", so the p travels
with the pair (WSTA in .stats) and gates its use at read time, not its record.

SL_Mode and TP_Mode inputs are REMOVED, and STOP_LOSS_MODE/TAKE_PROFIT_MODE
with them - deleted rather than left dangling, per the RISK_REWARD_RATIO rule:
a live enum with no input behind it is the shape of the 2026-07 incident where
a saved .set kept feeding a deleted ordinal back in. With the inputs gone there
is no ordinal left to feed, and ValidateTradeManagementInputs() loses two
members.

Three tiers at read time, most trusted first: the swept pair when it cleared
its own family-wise test; else the mean excursions (cruder, but nothing was
SELECTED to produce them, so they need no such test); else a fixed fallback
that announces itself and is unreachable on a deployed chart, since a chart
with no completed era cannot pass the deploy gate.

Entry offset, expiration, trailing and the exit-vote flag stay inputs for the
MT5 GA - they need entry-fill and path-stepping simulation this phase does not
have.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 14:28:34 -04:00
AnimateDread
1a14afb990 fix(topology): close the cold-start capacity floor once era 0 has written weights
The existing re-derive only fires when there are NO weights. That covers a
deliberate wipe-and-restart and misses the case that has now happened twice:
a cold fleet start sizes every topology against an empty pool, era 0 writes a
.nnw minutes later, and from that moment the floored .cfg is authoritative
forever.

Measured today: six charts booted 13:40:13-16, every census reported "0
compatible row(s) from 0 peer file(s)", and the first pool publish landed at
13:56-13:58 - sixteen minutes after the decision. SP500 and XTIUSD came out
with lstm 128->8 where the same charts ran 128->16 that morning on a census of
30,161 rows from 2 peers. Half the recurrent state, pinned.

The discriminator needs no new persisted state. The census only ever GROWS as
peers publish, so a .cfg pinning a SMALLER topology than today's measurement is
a .cfg written under a poorer census. Strictly larger only - shrinking a
topology under trained weights is what adopt-don't-compare exists to prevent.

It fires only when there is nothing to protect: not deploy-approved, and not
tier-ranked. Both come from .stats, read at the decision rather than assumed. A
deployed model is trading and an already-ranked one has completed a real pass
3; either way the weights are the asset and the larger topology is a retrain,
not a resize - so that case logs the comparison and keeps the weights. An
unranked, undeployed model has era-0 noise behind it, which is what makes
discarding it free. It cannot loop: after the rebuild the .cfg matches the
census.

Reuses the existing discard-and-rebuild branch verbatim - the only change is
which condition reaches it.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 14:06:53 -04:00
AnimateDread
f942cb2ef2 feat(exits): size the stop and target from measured excursions; gate indicators on a new bar
Two changes, both driven by the same single-backtest profile (6,607,688 ticks,
165 s, SP500 H4 2019-2026).

INDICATORS, 39% OF THE PASS. m_indicators.Refresh() cost 61.4 s at 9.30 us per
tick, running on every quote regardless of Expert_EveryTick. Every indicator
this EA registers is computed from CLOSED bars - ATR, the MA, the ZigZag, the
feature block - so their value cannot change between two ticks of the same bar
and the refresh was recomputing a constant. It is now gated on a new bar, with
its own CNewBar watermark: IsNewBar() consumes the transition and Refresh()
runs before Processing(), so sharing m_newBar would have silently disabled the
SetDirection gate.

The exit invariant holds. What changes intrabar is price and position, and
neither comes from an indicator buffer: m_symbol.RefreshRates() still runs on
every tick and is what CheckClose/CheckTrailingStop/pending maintenance read. A
trailing stop still moves on any tick; it now compares the live price against
an ATR from the last closed bar, which is the ATR it should have been using.
ProtectOpenPosition() keeps its ungated copy - it runs only on ticks Refresh()
declined, and never appeared in the profile.

SL_MEASURED / TP_MEASURED (both value 100), now the shipped defaults. The fixed
pair was SL x2 / TP x6 against a measured SP500 MFE of 2.59 ATR and MAE of
2.71: the stop sat INSIDE the average adverse move and the target BEYOND the
average favourable one, so the average trade was stopped out before reaching a
target it does not reach. That converts winners into losers mechanically at any
precision, and no model work can fix a barrier pair pointing the wrong way.

These are NOT the removed SL_INTELLIGENT/TP_INTELLIGENT. Those scaled the
barriers by the model's own CONFIDENCE - an over-confident model gave itself a
tighter stop and a wider target, which is why they were deleted. These read a
MEASUREMENT of what the market did on the bars this ensemble fired on: the mean
adverse and favourable excursions in ATR at the hold horizon, already computed
by the era report that certifies the deploy and previously printed and thrown
away. Only the WIDTH is measured; the reward:risk that falls out is reported,
never targeted - the ratio is policy, the width is what pays.

A FRESH ENUM VALUE, never the vacated -1 the removed members held: MetaTrader
does not validate enum inputs, so a .set saved by that build still feeds -1 in,
and reusing it would silently give a stale file a new meaning.
ValidateTradeManagementInputs() keeps rejecting -1 and now accepts 100.

Plumbing follows the derived-threshold route exactly, because
ExpertSignalCustom.mqh is the PARENT of the filter that owns g_ensBest* and
cannot read them: stashed inside the isBetter block (so the widths describe the
bars the CHECKPOINT fired on, never a later era's), persisted as WST9 in
.stats (a deployed ensemble runs no further eras - the tier-ladder failure one
layer along), and published per tick via PublishMeasuredBarriers(). The -1/-1
"not measured" state is published too, so a reset-weights cannot leave a stop
sized off a dead ensemble.

With no measurement it falls back to the shipped fixed presets and says so once
per run. It deliberately does not substitute a plausible number: an invented
width would be indistinguishable from a measured one in every log afterwards.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 13:31:30 -04:00
AnimateDread
04cc345661 perf(tester): split the per-tick profile so the slow bucket names itself
Three turns of reasoning about where a 6.3-minute pass goes have produced
three hypotheses and no measurement. The profiler that would answer it
already existed and printed nothing: across 129 optimization passes on 12
agents the "tester pass profile" line appeared ZERO times, while OnInit's
output appeared on every one.

Two fixes, both aimed at ending the guessing rather than at being right.

1. The line is now built by WarriorTesterProfileLine() and printed from
   OnTester() as well as OnDeinit(). OnTester runs on the agent at the end of
   the pass, BEFORE OnDeinit. If the line appears there and not in OnDeinit,
   an optimization agent is discarding OnDeinit's Print; if it appears in
   neither, g_tpTicks is genuinely 0 and the instrumentation never ran. Those
   need different fixes. The zero-tick case now prints its own explicit line
   instead of staying silent, because a profiler that says nothing when it
   fails is indistinguishable from a fast pass.

2. "Expert.OnTick" was one bucket containing both halves of the question.
   CExpertCustom::Refresh() runs on EVERY tick whatever Expert_EveryTick says
   - correctly, since an open position must be manageable on any quote - and
   under a 1-minute-OHLC model that body executes millions of times per pass.
   Its three steps are now timed separately: TCHasEnoughHistory(),
   RefreshRates(), and m_indicators.Refresh(). They are reported as a SUBSET
   of Expert.OnTick, not as siblings, because double-counting a bucket is how
   a profile lies. ProtectOpenPosition()'s copy of the same refresh counts
   into the same bucket rather than hiding on the declined-tick path.

The accumulators move to System\TesterProfile.mqh: ExpertCustom.mqh has to
see them and is included long before Warrior_EA.mq5's own globals. Every
bracket is guarded on g_tpActive, which OnInit sets only for
MQL_TESTER/OPTIMIZATION/FORWARD - a live chart never reads the clock for it.

No behavioural change to any path. Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 12:07:09 -04:00
AnimateDread
a9c21fdf10 feat(tester): gate input RANGES at OnTesterInit, not just input values
ValidateTradeManagementInputs() hard-gates input VALUES at OnInit because
MetaTrader replays a saved .set without validating it. Nothing gated input
RANGES, so an optimization would happily sweep a parameter that cannot affect
a trade - or one that destroys the run - and score every pass as if it meant
something.

Measured on the 2026-08-31 session: Signal_ThresholdOpen was swept over
10..80, which MetaTrader expands to twelve PERCENTAGE_PRESETS members. Its own
label reads "SEED (derived after era 1)": every pass restores the pinned rung
from .stats into g_ensDerivedThreshold, and OnTick publishes it over
m_threshold_open before the first bar closes. Twelve identical strategies, a
12x multiplier on a fifteen-hour estimate, and no warning anywhere. A wasted
dimension does not only multiply runtime - it fills the GA's fitness landscape
with plateaus, so crossover produces clones. 115 of 129 passes scored 0.

WarriorGuardOptimizationRanges() runs once in the controlling terminal, the
only place ParameterGetRange/ParameterSetRange are legal and the last moment a
bad sweep is free to stop. Two classes:

  REFUSE - keys BuildModelFingerprint(), or the capacity budget upstream of the
  neuron count inside it (Use_Training_Pool). These select a different MODEL,
  not a different strategy: the agent resolves a .nnw filename that does not
  exist and either votes silently or trains inside the backtest, and the pass
  still lands in the results table looking real. Session stopped with
  INIT_PARAMETERS_INCORRECT.

  PIN - a seed, or inert on an inference-only pass: the training-only inputs,
  the DB ranking pair, presentation flags, and the news filter (calendar error
  4806 over historical dates, so it fails open on every bar - the message says
  so, because a config optimised here trades through news live). The sweep is
  switched off and the operator's own value kept.

tradingdirection is deliberately NOT in the table - it is a legitimate
override, and WarriorEffectiveDirection() is explicit that the screen never
overrules an operator who chose. A degenerate range spanning BOTH and the
screened side is reported instead, from the controlling terminal where the
drift screen has full history.

An unresolved name warns rather than refuses: the table addresses inputs by
string, so a rename unbinds it, but a false refusal would make the EA
impossible to optimize at all. The summary line prints on every optimization,
clean ones included, so a table that has come unstuck shows up on the first
run - same doctrine as the drift screen reporting on charts it does not
restrict.

Compiled 0 errors, 0 warnings. Live paths untouched: nothing outside
OnTesterInit() is reachable from this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 11:52:42 -04:00
AnimateDread
818ffc7d8c docs(tester): the DLL guard never prevented the load - name the real restart blocker
Two comments claimed CNet::InitComputeDll()'s tester guard "avoids agent-side
file-lock/synchronization failures" and that an inference-only backtest "never
loads WarriorDML/WarriorCPU.dll". Both are false, and the second one is
contradicted three lines below its own sibling comment, which correctly notes
that MetaTrader rejects a missing DLL "at program load before any of this code
can run" - i.e. the #import is resolved before MQL5 runs, so the guard can only
stop CALLS, never the load.

Measured 2026-08-31: all twelve metatester64.exe agents had their own sandbox
copy of WarriorCPU.dll mapped and locked. Recompiling the .ex5 makes the
terminal re-push the program's dependency set to every agent; the DLL write
fails with Windows error 32 (ERROR_SHARING_VIOLATION) and the tester aborts the
session after three retries ten seconds apart. It presents as "the backtest
will not start", with a clean EA teardown and nothing in the tester journal.

No code change: MQL5 cannot unload an imported DLL. The comments now name the
failure, its log signature, and the operational remedy (kill the resident
agents after a recompile), so the next diagnosis does not start at OnDeinit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 10:18:31 -04:00
AnimateDread
b512b3aa18 perf(signal): gate the VOTE on a new bar, keep the VETOES per tick; cache the news filter
Operator: "nothing should run every tick if expert_everytick is false." Almost
right, and the exception is why this is a split rather than one guard.

VOTE filters (GetPatternCount() > 0 - the AI members) read dPrevSignal, which
RefreshConvergedSignal() only refreshes when a bar closes. Re-running them per
tick recomputes the same number from the same inputs. Now gated on CNewBar, with
their contribution carried in the member scratch array across the bar's ticks.
Journaling and the raw draw are skipped with the call, deliberately: both read
state only a real Direction() call refreshes, so running them against a cached
value would re-journal one bar's pattern under many timestamps.

VETO filters (GetPatternCount() <= 0 - news, session, risk guard) signal by
returning EMPTY_VALUE from that same call and what they answer CHANGES WITHIN A
BAR. An entry can be placed on any tick, so freezing them to bar granularity
would let a trade through a news window that had already opened - and the risk
budget is required to enforce at quote frequency, not per bar. They stay live. A
cached veto still vetoes for the rest of the bar. Expert_EveryTick=true disables
the gate entirely, which is what that input means.

The abort rollback now revokes only filters actually entered this tick:
RevokeVote() restores a BeginVote() snapshot, and revoking one that never ran
would roll it back to a stale one. An aborted pass also drops the cache.

THE NEWS FILTER WAS THE EXPENSIVE VETO, and it had no cache at all: one
CalendarValueHistory() per relevant country on every call. Now cached to the
minute, which is EXACT rather than an approximation - the window is [now-N,
now+N] and calendar events carry minute resolution, so the set inside it cannot
change until TimeCurrent() crosses a minute. Country codes resolved once.

And its failure Print was UNCONDITIONAL. Error 4806 (no calendar data for the
window) is the normal case in a backtest, so that was a journal write per tick
per country - journal I/O being the most expensive thing in a tester pass, the
diagnostic cost more than the filter it was diagnosing. Once per run now, and it
says plainly that the filter is INERT for the run, so a config optimised there
is one that trades through news.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 10:04:32 -04:00
AnimateDread
75406a5b04 perf(signal): stop building throwaway strings on every tick in Direction()
Direction() runs on EVERY TICK - the stdlib's m_every_tick only moves
StartIndex(), it does NOT gate CExpert::Processing(), which opens with
m_signal.SetDirection(). And Direction() RECURSES into every filter, so on this
fleet that is ~8 invocations per tick.

PrintVerbose() is a plain function, so its argument is constructed whether or
not VerboseMode is on. Two of the calls in that path are unconditional - a
concatenation plus IntegerToString, and another plus DoubleToString - so a
tester pass over years of history built and discarded millions of strings to
produce no output. Both are now behind if(VerboseMode); the rare error branches
keep the plain call, where a bare literal is not worth guarding.

The filter loop's `directions[]` was a local dynamic array, heap-allocated and
freed on every one of those calls. Now a member sized once, after which
ArrayResize() returns immediately.

Behaviour is unchanged in both cases - this is call-count work, not logic.

NOT DONE, and the earlier suggestion to do it was WRONG: caching Direction()
per bar is not behaviourally neutral. The veto filters (news, session, risk
guard) signal by returning EMPTY_VALUE from the same call, and they legitimately
change WITHIN a bar - a news window opening, a session closing, the risk budget
breaching. Freezing the vote to bar granularity would freeze those with it, and
the risk budget is required to act at quote frequency, not per bar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 09:26:22 -04:00
AnimateDread
634f8459b2 fix(train): the prebuild had five ways to refuse and told you none of them
SP500 sat wedged for 46 hours printing "Train() is taking the 'arming the first
label-cache prebuild' branch" every minute, 2810 times per member, and the log
could not distinguish an unsynchronised series from a cold ZigZag handle from a
still-settling history depth. StartLabelCachePrebuild() has five early returns
and exactly one of them printed.

m_prebuildBlockReason is now set at every return and printed by
ReportTrainStall() as its own line, with what to do about each: an
unsynchronised series names the bar count and says only the terminal can clear
it; a cold ZigZag distinguishes "fills in seconds" from "failed Create(), needs
a re-attach"; SettledBars names the number it returned against the depth it was
asked for. Cleared on the arming path so a stale reason cannot outlive the
block it described - a stale explanation sends the reader after a problem that
is already fixed.

Same defect as 29cab08 one commit earlier: a decision that cannot name its own
reason. That one blamed PRECISION for a by-side failure; this one blamed
nothing at all for two days.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 17:12:22 -04:00
AnimateDread
f7adb0bf16 feat(panel): Pause works on a deployed chart - it holds ONLINE LEARNING, and owes the bars back
Operator asked for a queue: hold online-training signals while an optimization
runs, then push them "as if the stop never happened". The queue already exists
and is better than a queue - it is a WATERMARK. m_onlineLearnedUpToTime
advances one LEARNED bar at a time inside the catch-up walk and is never
bulk-set at the end, so a step that returns early leaves it exactly where it
was; the next walk starts at the frontier, runs back to the watermark and
learns oldest -> newest in strict chronological order. It rides in .stats, so
it survives a restart. Nothing to serialize, nothing to drain.

What was missing was the switch. OnlineLearnStep() already gates on
m_trainingPaused - but HandleCpTogglePause() REFUSED on a deployed chart ("there
is no training run to pause"), which was true of the training loop and wrong
about the chart. Online continual learning runs only on deployed charts, so the
one state where pausing matters was the one state the button would not enter.

WHY IT MATTERS FOR THE GA: online learning re-saves the deployed .nnw, and every
tester agent re-seeds its optcache when the production file is newer than its
copy. A model that keeps learning mid-run makes later passes evaluate different
weights than earlier ones and the GA reads that as a parameter effect - and the
config you optimised was tuned against a model that no longer exists by the time
you deploy it. Pausing freezes the weights for the run and gives the bars back
afterwards.

Bounded by ONLINE_LEARN_MAX_CATCHUP (64 confirmed bars, ~10 days on H4) - the
walk stops there and older bars stay behind the watermark. Covers an
optimization run; not a way to park a chart for a month. Stated in the alert.

Trading is unaffected: nothing in the live signal path reads m_trainingPaused;
the only other deployed-state reader is the one-shot ladder/snapshot rebuild,
which defers. The alert names which thing was paused, because "training paused"
on a deployed chart reads as pausing something that was not running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:55:15 -04:00
AnimateDread
752fae9e02 fix(signal): keep the cooldown behind order validation - the arrow moves, the clock does not
Operator decision: reverse the reversal. a60cfbc stands - VoteCooldownCommit()
fires only once OpenLongParams/OpenShortParams validate, so a broker rejection
never burns the spacing budget a real trade needs. Those failures are transient
(ATR warm-up, stops-level), not a property of the vote, and paying a full
cooldown for one costs a tradeable signal.

The arrow stays where 32afb00 put it, at the point the SIGNAL is declared, so
the first-order coupling is still gone: no arrow is erased by a stop it never
asked for, and the live layer no longer contradicts the overlay reconstruction.

Residual coupling documented rather than left to be rediscovered: the arrow is
still GATED by VoteCooldownAccept, and what that clock holds depends on which
orders validated - so while order shaping keeps failing, consecutive bars can
each draw an arrow. Second-order, and only while orders are failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:50:42 -04:00
AnimateDread
32afb003f1 fix(chart): an arrow is a statement about the VOTE, not about trade management
The live filtered view drew the arrow AFTER OpenLongParams/OpenShortParams
validated and ERASED it when they failed - invalid SL/TP, stops-level, ATR
warm-up, unsynced swing history. So the same vote history drew a DIFFERENT
arrow set under a different SL_Mode, which makes the chart a picture of the
trade policy instead of the signal.

It also made the two layers disagree by construction: the overlay
reconstruction has always drawn on vote + threshold + direction alone, with no
OpenParams anywhere. Live arrows and replayed arrows were answering different
questions in the same namespace.

Trade-management effects are measured in a real backtest and in the tester GA -
never on this chart and never in the era report, which already states it pays
"no stop and no target - the SIGNAL, not an exit policy, and it GATES NOTHING".
Training was already clean: zero trade-management references in Training.mqh,
Labels.mqh or Labeling/.

The signal is now declared at one point - vote cleared, direction allows,
declustering kept it - and the arrow and the cooldown commit both happen there.
They moved together deliberately: an arrow with no cooldown behind it draws a
second arrow on the next bar and reintroduces the clustering
CSignalDeclusterPolicy exists to prevent.

ONE BEHAVIOURAL CONSEQUENCE, stated rather than buried: an order-shaping
failure now CONSUMES the cooldown, so the retry RevokeVote allows is spaced
like any other signal instead of being re-offered on the very next bar.

SL/TP dropped from the arrow tooltip and from DrawVoteArrow's signature for the
same reason; it reads "SIGNAL BUY @ price | vote N >= M". EraseVoteArrow()
removed - its only caller was the failure branch. The overlay sweep still
deletes an arrow on its own verdict, which is a statement about the vote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 10:54:37 -04:00
AnimateDread
29cab08765 fix(gate): the refusal blamed PRECISION for a by-side failure - with numbers that PASS
tradeableOK = voteGate.tradeable && bothSidesPay, but the stage-3 refusal only
knew how to attribute the voteGate half. Its chain is coverage -> one-sidedness
-> PRECISION, where PRECISION is the FALL-THROUGH. So a best era that failed
only because a side loses money landed in the fall-through and printed:

  SP500: "THE BEST ERA FAILED ON: PRECISION - 51.1% against a 21.8% bar"

51.1% clears 21.8% by 29pp. The real failure was a short book losing 1.00 ATR
on every call it made. Operator caught it by noticing SP500 has HIGHER
precision than XTIUSD (48.4%) and still would not deploy.

Same defect class as the finding that motivated the by-side rule in the first
place: the quantity was computed, printed elsewhere, and not consumed by the
thing that reports the decision. Adding a condition to a gate is not finished
until the refusal can name it.

Now stashes the best era's by-side expectancy alongside the coverage/precision
numbers it already kept, and names the losing side explicitly - including the
LONG and SHORT case. Placed BEFORE the precision fallback, because a
fall-through must never be reachable by a condition that has its own name.

Also reverts TrainingOptimizer / OOSSplit / VolumeData / NewsFeatureWindowMinutes
to inputs (operator decision): they are "train a different model" choices and
stay operator-facing. The comment now says what actually matters about them -
never put one in a tester GA sweep - rather than hiding them. Indicator periods
were never the issue: the AI MA feature already reads the auto-tuned value via
CFeatureBuilder::InitMA -> m_indicatorTuner.maPeriod, never the raw seed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 10:26:07 -04:00
AnimateDread
371eb184fb feat(direction): act on the drift instead of only printing it; retrain-forcing knobs are no longer inputs
THE DRIFT SCREEN. The deploy gate already refuses a chart whose one side loses
on every call, and its refusal names the remedy: "switching it off with
tradingdirection raises expectancy arithmetically". Nothing acted on it -
tradingdirection is hand-set and defaults to BOTH, so the drift was measured,
printed, and ignored.

Expert/Signal/DriftScreen.mqh walks every bar's own open and close:
  meanNet = mean(close-open), meanAbs = mean(|close-open|), ratio = net/abs
"the average bar's net direction as a share of the average bar's size" -
scale-free, so one constant serves the fleet. No fitted term, no cost term, no
significance test: at 15-50k bars everything is significant, and the question
is whether the drift is BIG ENOUGH to give up a side, not whether it is real.

Measured on H4 across both archived feeds per instrument:
  SP500  +4.04% / +3.65%   up 53.8% / 52.5% of bars   <- the only one over 3.5%
  FTSE   +4.20% / +2.58%     USDJPY +2.94% / +1.04%
  USDCAD +2.24% / +0.22%     XAUUSD +1.88% / +2.61%
  WTI    -0.74% / -1.14%   (the only negative)
The two feeds for one instrument disagree by up to 2pp, so the MAGNITUDE is
feed-dependent and only the sign replicated - hence one shared threshold set
where only a large, both-feed-agreeing drift crosses, not a per-symbol table.

At 3% exactly one fleet symbol crosses: SP500. That matches the gate's own
by-side expectancy at the deployed rung (SP500 short -1.00 ATR/call, while
XTIUSD short pays +0.13 and so stays two-sided despite a mild negative body
drift). Two independent measurements, same verdict.

It resolves through WarriorEffectiveDirection() - the existing single
resolution point - so the sweep, chance rate, deploy gate, live path and
overlay all pick it up and the gate re-certifies the one-sided book for free.
An explicit tradingdirection always wins; the screen can only narrow BOTH.
Printed on every chart including the ones it does not restrict, because a
screen that only speaks when it fires cannot be calibrated.

THE BLOCKED SIDE STILL EXITS. Verified, not assumed: the direction filter sits
only in CheckOpenLong/CheckOpenShort, while CheckClosePosition reads the raw
signed m_direction. So a Sell vote on a long-only chart cannot open a short but
does close the long, once Exit_On_Reversal_Vote is on.

RETRAIN-FORCING VALUES ARE NOT INPUTS. TrainingOptimizer, OOSSplit, VolumeData
and NewsFeatureWindowMinutes each key a fingerprint and none is a feature
enable flag. A GA sweeping any of them re-keys every .nnw, restarts all four
members from era 0 and destroys the deployment it was tuning - silently, since
a new key is indistinguishable from a first run. All four verified at their
defaults on the six live charts first, so the fingerprints are byte-identical
and no retrain is forced. Enable* flags stay inputs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:58:33 -04:00
AnimateDread
eed262c416 fix(chart): the vote-arrow file stamped the threshold from INIT, not the one in force
An arrow is a claim about a threshold, so the store discards its cache when
that threshold changes between sessions. Correct rule, stale input: the stamp
was captured once in Configure() during OnInit and never refreshed, while the
derived rung goes on MOVING for the rest of the session (it follows each era's
rung until a checkpoint pins it).

So the two files written in the same OnDeinit disagreed by construction:
.stats saved the CURRENT threshold, .votearrows saved the init-time one. Next
start compared them and threw away a perfectly good arrow set.

Measured 2026-08-27 22:26 - four of six charts lost everything:
  XAUUSD stored 25 / now 20   XTIUSD stored 25 / now 5
  SP500  stored 25 / now 5    USDJPY stored 15 / now 20
The two that survived, EURUSD (173 arrows) and USDCAD (13), were the two
whose rung happened not to move.

WarriorVoteArrowThreshold() is now the one expression, read live at the load
comparison AND at every save; the cached member is gone, so it cannot go stale
again. Only the close threshold is still passed to Configure(), because
VOTE_EXIT_DISABLED_THRESHOLD genuinely cannot move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 22:37:49 -04:00
AnimateDread
9eda06d2c5 fix(gate): expectancy is the bar - drop the alpha test and the cost term
Two operator corrections, both reversing decisions I made this session.

1. THE BAR IS POSITIVE EXPECTANCY, NOT PROVEN EDGE.

   f999139 judged each side on ALPHA - payoff minus that side's own zero-skill
   book. That is the prove-your-edge pattern, which was backed off as
   ineffective, and this project has the receipts: the deploy gate once asked
   ~66% win rate on an OOS window holding ~63 effective observations, a bar no
   reachable signal could clear.

   Alpha rejects a side that reliably earns +0.08 ATR a call for the sin of
   sitting in an instrument whose drift earned more. A book cannot trade the
   drift under prop drawdown rules, so that is not a comparison the account can
   act on. A side is now judged on one thing: does it make money.

   Alpha is still MEASURED and REPORTED beside the refusal, because it is what
   says whether the expectancy survives the drift turning over. That is a
   position-sizing input, not a gate.

2. NO COST TERM. My first pass at this subtracted a round-turn spread. The only
   spread available at that point is a single live SYMBOL_SPREAD snapshot taken
   whenever the era happens to end - inflated many-fold near rollover, and
   applied to historical calls that traded at entirely different spreads. It is
   a noisy reading, not a cost model, and these cost figures have never worked.
   Baking one into the gate buys false precision and moves the answer for
   reasons unrelated to the signal. The spread is still printed beside the
   payoff to be judged by hand.

   spreadR now appears at exactly two sites, both of them Print arguments. It
   decides nothing.

Replayed on the measured per-rung curves:

  EURUSD  10%   +0.16 / +0.07
  XTIUSD   5%   +0.16 / +0.05
  USDCAD  REFUSE  short loses at every rung (-0.13 / -0.11 / -0.10)
  XAUUSD  REFUSE  short loses at every rung (-0.46 / -0.53 / -0.59)
  SP500   REFUSE  short loses at 5% and 10%; its 15% rung fires short n=0 and
                  twoSided bars it inside the rate gate

Same three refused as under alpha, so on this data the two rules agree on the
verdict and differ on the reason - but the reason is what the operator acts on,
and "this side loses money" is actionable where "this side underperforms
buy-and-hold" is not. The refusal names the losing side so tradingdirection can
switch it off, which raises expectancy arithmetically.

Also fixed two comments left stale by the change - the hoist notes still
described the alpha and cost tests they no longer perform, which is the exact
defect class this session has been unpicking.

Compiles clean, 0 errors 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 22:15:08 -04:00
AnimateDread
f999139156 fix(gate): judge each side on ALPHA, not raw payoff - the raw test had it backwards
Correcting what I shipped in b12c2c0. The by-side gate tested "is this side's
payoff positive". It should test "does this side beat its OWN zero-skill book",
and on real data the two give opposite answers.

zeroSkill is the always-long book over the same bars; always-short is exactly
its negative, so alphaLong = meanLong - zeroSkill and alphaShort = meanShort +
zeroSkill. The raw test credits drift as skill on one side and charges it as
failure on the other.

MEASURED, six charts, hold horizon, at each chart's derived rung:

  chart   zeroSkill   rawL    rawS      alphaL   alphaS
  EURUSD    +0.046  +0.073  +0.115     +0.027   +0.161
  USDJPY    +0.149  +0.358  -0.136     +0.209   +0.013
  USDCAD    +0.096  +0.177  -0.070     +0.081   +0.026
  XAUUSD    +0.436  +0.076  -0.379     -0.360   +0.057
  XTIUSD    +0.089  +0.222  -0.065     +0.133   +0.024
  SP500     +0.393  +0.657  -0.273     +0.264   +0.120

EVERY short book reads negative raw and POSITIVE on alpha: they lose ATR because
they fight the drift, and still beat always-short. The one side with no alpha is
XAUUSD's LONG - the drift side - where the model picks worse-than-average entries
on the most strongly trending instrument and the raw test called it healthy.

So the raw test rejected five charts for holding alpha merely smaller than beta,
and passed the one side that had none. It also made "trade only the drift side"
look like the fix, which is the strategy this project already measured into the
ground: the SQX replication scored its entry signal at t = -3.40 with +0.69
correlation to the underlying on 14/14 feeds - "a long position with a trade
schedule" - and recorded that such a book does not diversify, its shared factor
surfacing in the month that breaches a drawdown limit.

Replayed on the per-rung curves, the corrected gate certifies:
  EURUSD 15%  (alpha +0.10 / +0.03)
  XTIUSD 10%  (alpha +0.08 / +0.03)
  USDCAD, XAUUSD, SP500 - no rung has alpha on both sides -> REFUSED
SP500's 15% rung has alpha on both sides but fires short n=0, and twoSided bars
it inside the rate gate before the payoff test is reached.

No new machinery: one subtraction inside the existing helper, the zero-skill mean
hoisted above the selection loop that now reads it, and the sweep line switched
from L/S to aL/aS - printing raw while gating on alpha is how an operator
concludes the gate is broken.

Compiles clean, 0 errors 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 21:36:31 -04:00
AnimateDread
5304cfb876 refactor: delete the machinery I added that was not earning its place
Self-audit against YAGNI. Three things from this session's commits were
configuration nobody asked for, and one was a measurement report pasted into
source.

DELETED - SignalCooldownScopeOverride + WarriorSignalCooldownScope(). I added a
source override for the cooldown scope, then concluded in the very next commit
that source-overrides-operator is the wrong pattern and stood it down to -1.
What was left was a mechanism whose only state is "disabled" - the definition of
speculative generality. The scope is read straight from its input now. The thing
that actually solves "which value is running" is the init line that PRINTS the
resolved value, not a second place to set it.

DELETED - REQUIRE_BOTH_SIDES_PAY. A #define that was always true, added so the
new gate could be "reverted". Nobody asked for that switch and a gate condition
that is optional is not a gate condition. The rule is either right or it is not;
if it turns out wrong, git is the revert mechanism.

TRIMMED - 38 lines of comment in the ensemble verdict listing all six charts'
by-side payoff numbers. Those are a MEASUREMENT: they belong in the commit that
made the change and in the session record, not pinned in source where they go
stale on the next run and start actively misinforming. Two lines left saying
what the code does and when it can be false.

TRIMMED - the cooldown override comment from 11 lines to 4. Same reasoning.

KEPT, with the case for each:
  CSignalDeclusterPolicy   replaced FOUR copies of one rule that had silently
                           diverged into two different units and two different
                           windows. Net negative lines.
  PivotLabelFillTarget     replaced FIVE hand-rolled target vectors and is the
                           only place the sum-to-1.0 gradient invariant lives.
  PivotLabel* window fns   the -1 sentinel collision they fix was a live defect.
  CRunningMean             two meanings were sharing one accumulator, which is
                           how the purge came to be sized off the wrong one.
  LABEL_RESOLUTION_CAP_BARS  a bound on a MEASURED quantity feeding the training
                           split. The floor is load-bearing; the cap stops a
                           degenerate ZigZag eating the training set.
  Test_PivotLabelWindow    161 of the 195 net added lines. Two of the three
                           things it pins were live bugs this session.

Net across the whole session, production code only, excluding tests:
+479 / -390 = +89 lines, and in exchange: decluster rule 4 copies -> 1,
target builders 5 -> 1, chart reconciliation 2 -> 1, twelve loose NMS members
-> two policy objects.

Compiles clean, 0 errors 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 21:02:04 -04:00
AnimateDread
b12c2c0ac0 fix(gate): pick the rung that PAYS on both sides, not the one with the best precision
Completes the previous commit. Gating the final verdict on by-side payoff was
right but incomplete: it refused the era instead of choosing a rung that works.

THE RUNG SELECTION WAS SELECTING FOR ONE-SIDEDNESS. It walked down from the top
and took the highest rung clearing the RATE gate. Precision rises with the rung;
drift-free payoff falls. Both are the same mechanism - a higher rung means more
member agreement, and on a trending instrument more agreement means more
agreement WITH THE DRIFT. Visible in the counts: SP500 fires short n=0 at its
15% rung, XTIUSD is 415 long / 94 short.

So the rule "take the highest rung that clears precision" was, on this fleet,
"take the rung where the short book has been squeezed out". The per-rung by-side
payoff was already computed (sideHlong/sideHshort, printed as the CONVICTION
CURVE) and the selection loop never looked at it.

It does now: walk down, stop at the first rung that clears the rate gate AND
pays on both sides. Replayed against the measured curves from this session:

  EURUSD  15% -> 10%   both sides pay (+0.157 / +0.075)
  XTIUSD  10% ->  5%   both sides pay (+0.161 / +0.049)
  USDCAD  15% ->  --   no rung pays both sides -> REFUSED
  XAUUSD  10% ->  --   no rung pays both sides -> REFUSED
  SP500   10% ->  --   no rung pays both sides -> REFUSED

Two charts certified at an honest operating point instead of five certified at a
dishonest one. That is a better outcome than the blanket refusal the previous
commit alone would have produced, and it is the same information - the earlier
version just could not act on it.

WarriorRungBothSidesPay() is the ONE rule, read by the selection loop, the final
verdict and the sweep report. A floor at zero, not a significance test: no
per-side variance is tracked here, so "positive" is the honest claim.

A side that never fired at a rung is not judged - but that cannot certify a
long-only book, because twoSided (firedLong > 0 && firedShort > 0) already bars
such a rung inside the rate gate. SP500's 15% rung is exactly that case and is
rejected before the payoff test is reached.

FALLBACK CHANGED so the refusal is legible: when no rung pays both sides the era
is reported at the rung the RATE gate alone would have taken, so the message
reads "here is the rung you would have traded and here is what its short book
did" rather than naming a rung the operator never saw.

The threshold sweep line now prints L/S per rung with a SIDE-FAIL marker, so
[DERIVED] moving down between eras is self-explanatory instead of arbitrary.

Compiles clean, 0 errors 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:41:38 -04:00
AnimateDread
eac8912dd0 fix(gate,cooldown): both sides must PAY, not just fire; stand down the source overrides
Three fixes off the 2026-08-27 session logs (six H4 charts, fresh from era 0).

1. THE DEPLOY GATE NEVER READ ITS OWN DRIFT-FREE TEST.

   twoSided was `firedLong > 0 && firedShort > 0` - an anti-degeneracy check.
   The by-side PAYOFF prints three lines below it under the heading "THE TEST
   THAT IS DRIFT-FREE", and nothing consumed it. Measured at each chart's own
   derived rung, hold horizon, ATR per call:

     EURUSD  long +0.073  short +0.115   <- the only one where both pay
     USDJPY  long +0.358  short -0.136
     USDCAD  long +0.177  short -0.070
     XAUUSD  long +0.076  short -0.379
     XTIUSD  long +0.222  short -0.065
     SP500   long +0.657  short -0.273

   Five of six were stamped DEPLOYABLE on 30-37% precision (chance 13-14%)
   while their short book lost money on every call. XAUUSD's whole vote earned
   -0.157 against its own always-long book at +0.436 and still passed.

   Precision is measured against the LABEL: it says a turn was called, not that
   the leg after it paid. On a trending instrument the down-legs are shorter, so
   a symmetric caller with real skill still bleeds on one side - and the higher
   rungs make it worse, because more member agreement means more agreement WITH
   the drift (SP500 at the 15% rung fires short n=0).

   tradeableOK now ANDs in bothSidesPay. A floor at zero, not a significance
   test - no per-side variance is tracked, so "positive" is the honest claim.
   A side that never fired is not judged; an era where neither side has a
   forward window is not judged either. REQUIRE_BOTH_SIDES_PAY restores the old
   behaviour. The verdict line now NAMES this when it withholds DEPLOYABLE,
   because "not good enough yet" and "one side of the book loses money" are
   different problems with different fixes.

   Timing is deliberate: ENSEMBLE_CHECKPOINT_MIN_ERA is 20 and the fastest chart
   is at era 17, so nothing has deployed yet and this costs nothing to land now.

2. THE SOURCE OVERRIDE WAS DISCARDING THE OPERATOR'S COOLDOWN.

   SignalCooldownOverrideBars was 30. The charts are configured for 10. The
   session log says "signal cooldown (30 bars)" 43 times across all six. That is
   the same failure the override exists to fix, pointed the other way - source
   defeating the operator instead of a stale profile defeating source - and it
   is WORSE, because a stale profile is at least visible in the chart's own
   inputs dialog and this is not. Stood down to 0, which is the exit condition
   its own comment always described.

   The scope override (added earlier this session) is stood down to -1 for
   exactly the same reason rather than kept out of convenience.

   The real fix for "which value is running" is not a second place to set it:
   ConfigureAISignal now prints the RESOLVED window and scope once per chart
   alongside the chart input and both overrides, and says so explicitly when
   they disagree. One line kills the whole class.

3. The overlay sweep's hard-coded OVERLAY_NMS_WINDOW of 6 is gone with the
   window/decluster work replayed onto main - it now uses the resolved cooldown
   like every other layer.

MEASURED, and it corrects a number I reported earlier in the session: against
the 30 bars the override was forcing, 89.8% of consecutive vote-arrow pairs sat
inside the window. Against the 10 bars actually configured, the figure is 11.7%
(1038 pairs across six .votearrows sidecars, gaps counted in real H4 bars with
weekends excluded). The earlier figure was true of the window that was running
and NOT of the one the operator set; the 10-bar number is the one to judge the
fix against. USDJPY carries most of what remains (18.4%, and all 21 fleet pairs
below 7 bars) - it is also the least-trained chart, at era 3.

NOT CHANGED, and flagged rather than fixed: the source default is still SCB_30
and its comment argues 20 is the smallest defensible value, because a trade on
this label is held 5 + the median leg = 18-19 bars. A 10-bar cooldown re-announces
inside that hold. That is the operator's call, not a bug.

Compiles clean: EA and both test suites, 0 errors 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:02:47 -04:00
AnimateDread
5d3f2367d1 fix(label): the purge was sized off the OVERLAP SPAN, not the resolution lag
Found while closing out the window shift. Two fields carried the wrong one of
two quantities the 2026-08-26 rewrite had explicitly separated in prose:

  OVERLAP SPAN   how many labelled bars share one pivot event. Deflates
                 standard errors. Equals the window width (5).
  RESOLUTION LAG how far ahead the newest bar a label READS is. Sets the
                 purge/embargo. The witness sits ~a ZigZag leg out: 13-14.

That rewrite's own comment says "resolution lag is a different quantity and no
longer belongs here" - and then CalibPurgeBars() went on reading the overlap
one anyway, because both lived in the same accumulator under a name
(MeanLabelLifespan, headed "mean bars-to-resolution") that fit neither.

TWO CONSEQUENCES, both measured against the shipped split (30% OOS, 15% calib
band, ~7000 IS bars, ~1000 band bars):

1. THE PURGE UNDER-COVERED BY ~9 BARS AT BOTH BOUNDARIES. The newest IS bar's
   label reads ~9 bars into the calibration band; the newest calibration bar's
   label reads ~9 bars into OOS. That is 0.86% of calibration observations -
   immaterial to the fitted threshold, and NOT an IS->OOS leak (those are
   separated by the whole band). Fixed anyway: it costs 18 IS bars out of ~7000
   (0.26%), and carrying a purge that provably does not cover its own dependency
   is the kind of thing that later gets cited as "the split was clean".

2. m_labelResolveAge WAS THE SAME BUG, and this one is not cosmetic. The field
   is documented as "the bar its pivot pair committed on" and both consumers
   read it that way, but it was assigned the lifespan:
     * TrainPoolResolvedMs() is the POOL PURGE KEY - "a peer row must not carry
       the future into this fit". It claimed every label was knowable 5 bars
       after its entry when the truth is ~13-14, so the pool could serve a row
       to another chart that had not yet reached the point where that label
       existed. Cross-instrument lookahead.
     * COnlineLearning's OOS payoff replay uses it as the EXIT bar, so it was
       measuring at a 5-bar horizon - the one the oracle showed is worth ZERO
       (perfect foresight earns ~0 at 5 bars, +1.2 ATR at 19). The corrected
       value moves it toward the real hold.

LabelResolutionBars() is now MEASURED: SwingPivotDirectionLabel records
idx - witnessIdx, accumulated on every resolved bar (it is a property of the
instrument's ZigZag geometry, not of the IS/OOS split, and the purge has to be
right on the first era). Floored at the window width so it can never be tighter
than the shipped behaviour and capped at LABEL_RESOLUTION_CAP_BARS 64 so a
degenerate ZigZag cannot eat the training set. Every other consumer
(MiShiftPad, ReportFeatureHealth's sampling range, an AutoTune log) is a
diagnostic that reports the value it used; none is on the inference path.

CRunningMean extracted from CLabelOverlap, which was already exactly that plus
the ESS formula. Two meanings sharing one accumulator is what made this
invisible for a month; they are now two named instances of one type.

Also adds Tests\Test_PivotLabelWindow.mq5 pinning the window geometry, the
distance-slot mapping and the sum-to-1.0 target invariant - the two defects this
branch already fixed (the -1 sentinel collision, the five hand-rolled target
vectors) are regression cases in it. Both suites compile clean.

VERIFIED: compiles 0 errors 0 warnings; assertions independently mirrored and
all pass. NOT VERIFIED: the suite has not been EXECUTED in the terminal, and the
label's behaviour against a real ZigZag buffer is still only checked by the
first era's prebuild census line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:57:50 -04:00
AnimateDread
184de331fc fix(label,signal): move the pivot window into the leg; one declustering rule, measured in bars
Two independent defects, both about a rule written down more than once.

THE LABEL WINDOW SAT ENTIRELY ON THE APPROACH. It labelled d=1..5 bars BEFORE
the turn. Measured payoff by distance, fleet-pooled over 102 era rows and 3
charts at the hold horizon: d=1 +2.095, d=2 +1.743, d=3 +1.300, d=4 +0.969,
d=5 +0.769 ATR/call. Slope -0.343 per bar, monotone on all three individually.
Pure hold truncation predicts d=5 earns 14/18 = 0.78x of d=1 (~1.63); it earns
0.769, which is 0.37x - so truncation explains less than half and the rest is
the ADVERSE APPROACH, the call sitting through bars of price moving against it.
Every bar of lead the window granted was paying that.

The window now straddles the turn: d=+1..-3 (LEAD 1, LAG 3). It keeps the
best-paying approach bar and spends the rest of its width on the far side,
where direction is confirmed. WIDTH IS UNCHANGED AT 5, deliberately - it sets
the class balance, and ApplyLogitAdjustment's tau is capped at 1.2 logits with
the cap binding at EVERY imbalance, so only the uncorrected residual moves
(1.9x at width 5). Simulated over three leg-length regimes the balance shifts
by <=0.2pp, so nothing downstream of it had to move. That is also why the
target stays FLAT across the window rather than Gaussian-tapered: a taper
shrinks directional mass ~40% and tau has no budget left to absorb it.

Finality is re-derived, not weakened. One committed pivot strictly newer than
the window's newest slot freezes the whole window - the labelling pivot's
finality and the negative verdict together - because ZigZag can only ever move
its newest pivot, and only toward newer bars. Verified: the witness is the next
pivot newer than the labelling pivot in 100% of simulated cases, so minMove
still measures the leg AFTER the turn.

TGT:PVT1 -> TGT:PVT2:lead:lag. Every .nnw re-keys; the fleet retrains from era 0.

-1 IS NO LONGER A VALID SENTINEL - it is one bar into the leg. Added
PIVOT_LABEL_NO_PIVOT and PivotLabelDistanceSlot(); the by-distance payoff report
bucketed the no-pivot population and the leg bars into the same slot otherwise,
and its "directional labels only" test (`d >= 0`) would have dropped every leg
bar it now exists to measure.

THE DECLUSTERING RULE WAS WRITTEN OUT FOUR TIMES and the copies disagreed.
Rules 0-3 lived in NmsLiveAccept, PruneDirectionalClusters, pass 3's OOS tally
and the overlay sweep, each carrying a comment insisting it must match the
others. It did not:

  * the two sweeps compared bar INDICES; the live gates compared wall-clock
    seconds. Elapsed time is always >= bars*period because weekends and session
    breaks add time without adding bars, so the live rule was strictly the most
    permissive and could only ever UNDER-suppress. On H1 a 30-bar window is 30
    hours against a ~50 hour FX weekend: a Friday signal never blocked a Monday
    one, once a week, per chart. On session-break instruments, daily.
  * the overlay sweep had rules 1 and 2 only - no cooldown, no alternation -
    against a hard-coded OVERLAY_NMS_WINDOW of 6 while the live gate required
    30, and its comment claimed parity with m_signalClusterWindow. It drew
    reconstructed arrows 7 bars apart onto a chart whose gate requires 30.

Now one CSignalDeclusterPolicy, measured in bars via WarriorBarsBetween(), with
an unresolvable frame SUPPRESSING rather than passing. The vote layer configures
it as a pure cooldown because that is what CheckOpenPosition actually applies.

Three more that let clusters through:
  * THE CLOCK WAS NEVER SEEDED. MT5 restores arrows from the .chr profile but
    not the state that spaced them, so every restart, recompile or timeframe
    change let the next bar fire regardless. Seeded from the newest arrow on the
    chart once the progressive restore completes.
  * THE COOLDOWN WAS CONSUMED BEFORE THE TRADE EXISTED. VoteCooldownAccept both
    tested and committed, ahead of order-parameter validation - and the failure
    branch restores the vote for retry but could not un-burn 30 bars. Split into
    a pure test plus VoteCooldownCommit() at the draw.
  * SCOPE HAD NO SOURCE OVERRIDE. Bars did; scope did not, so a chart whose
    profile held PER_DIRECTION silently dropped rule 0 with no way to correct it.

Also DRY: five hand-rolled 3-class target vectors -> PivotLabelFillTarget(), the
one place the slot order, the smoothing constants and the sum-to-1.0 invariant
live (the gradient is target_i - softmax_i, so a vector that does not sum to 1
adds a constant drift to all three logits). Two copies of the chart-wide
reconciliation -> WarriorReconcileVoteCooldown(). Twelve loose NMS members ->
two policy objects.

Documented at CNet::backProp: sampleWeight = 0 IS NOT A SKIP. Adam still applies
decayed momentum and decoupled weight decay, t still advances, and m_batchCount++
is unconditional - so masking a class-skewed subset that way shrinks every weight
rather than ignoring the example. Drop the bar before queueing instead.

Compiles clean (0 errors, 0 warnings). Window arithmetic verified by simulation;
the label's in-situ behaviour against real ZigZag output is NOT verified here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:57:50 -04:00
AnimateDread
213b3aac15 fix(chart): the reconciliation never ran - it was hooked to a sweep deployed charts do not do
cooldown-recon put the chart-wide cooldown at the end of the overlay sweep. It
executed ZERO times. This store's own header already said why: the sweep
're-arms only when an era ends. A DEPLOYED ensemble runs no further eras'. Five
of six charts were deployed, so there were zero 'Filtered view: swept' lines in
the entire session while the saved files still held 148 same-side pairs under 30
bars on XTIUSD.

Moved to the completion of the progressive vote-arrow restore, which runs on
every chart including deployed ones.

The restore thinning alone was never going to be enough either: MT5 persists
chart objects in profiles\Charts\*\chart*.chr, so arrows drawn under an older
window are ALREADY on the chart when the process starts, and a freshly-thinned
restore just adds to them. Two correctly-thinned sets still union into clusters.
The chart is the only authority.

Same construction as before: OBJ_TREND only (the line is the canonical half of a
mark, matching Snapshot()), sorted by time first because object order is not time
order, and the gap>0 guard so a mis-ordered set fails visibly by keeping rather
than silently by deleting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:16:40 -04:00
AnimateDread
4e2cdd3aff fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.

Two independent thinning passes over one namespace produce a union, not an
intersection.

MEASURED from the saved .votearrows files, which is what the chart actually
holds:

  XTIUSD  284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
  XAUUSD  263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
  EURUSD  309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0

while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.

The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.

OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
AnimateDread
844aac653a fix(train): the OOS final pass ran a full epoch at an undecayed rate
Capping the pass at m_etaCeiling was not enough. Measured on the first two live
runs: USDCAD 0.00085 over 13,335 bars, EURUSD 0.00242 over 15,041 - a 3x spread
across charts, because a chart whose plateau ladder reset recently still carries
a high eta and the cap never bound.

The slice turns out to be roughly HALF the data, not a tail, so one pass over it
at the model's own rate is a full training epoch on a model that has already been
selected and certified. That is materially more than the 'just a bit finer
weights' this was asked for.

OOS_FINAL_PASS_ETA_SCALE (0.25) now scales the rate. Scaling rather than
shortening the pass keeps the whole slice in play - seeing the held-out bars at
all is the point - while making the step proportionate to an already-selected
model.

USDCAD and EURUSD have already taken the unscaled pass; that is not reversible
without a retrain. USDJPY has not converged yet and will get the corrected one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 13:46:48 -04:00
AnimateDread
0dda31534e fix(chart): purge stale member arrows even when there is no sidecar to restore
The purge was gated on m_arrowRestorePending, so it only ran when the .arrows
sidecar had something to load. Stale arrows do not come from the sidecar - MT5
persists chart objects in profiles\Charts\*\chart*.chr independently of it. A
chart carrying member arrows from a session where DrawUnfilteredSignals was ON
would therefore never be cleaned, because the sidecar it would have needed is
gone.

Currently inert: there are zero .arrows sidecars on disk. Which is also the
correction to the diagnosis in c5b9a1a's message - the '264 arrows restored' that
prompted it was a MISREAD of 'queued 264 combined-vote arrows', so member arrows
were never the cause of the reported clutter. The gate remains correct as
defence; it was not the fix. The window was.

Also silences the line when there is nothing to skip and nothing to clear: it
runs on every chart on every start, and six 'removed 0' lines are noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:09:04 -04:00
AnimateDread
cc45451a22 fix(signal): the source override was applied before the minutes input, which could defeat it
Signal_CooldownMinutes is a per-chart input like Signal_CooldownBars, and it ran
AFTER SignalCooldownOverrideBars - so a chart carrying a stored minutes value
would have silently defeated the override, which is precisely the problem the
override was added to solve.

Latent rather than live: every chart currently holds SCM_OFF. That is the kind of
'works today' that stops working the first time someone sets the input, and it
would have looked like the override simply not functioning.

Minutes resolve first; the const now has the last word.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:06:10 -04:00