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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Raising Signal_CooldownBars from 10 to 30 changed nothing. All six live charts
kept reporting a 10-bar window, because MT5 stores an input PER CHART in
profiles\Charts\*\chart*.chr and an already-attached EA ignores a changed
default entirely. This codebase already documents that trap, in the derived-
threshold comment in Training.mqh - and converting SignalClusterWindow from a
const to an input reintroduced the exact problem the const existed to avoid.
SignalCooldownOverrideBars (const, 30) now wins over the input; 0 hands control
back to the panel. Tunability per chart is kept, source-correctability is back.
Not applied when the input says OFF: an operator who switched the cooldown off
meant it, and silently re-enabling it from source would be the same surprise
pointed the other way.
ALSO gates the per-model arrow restore on DrawUnfilteredSignals. DrawObject()
returns early when the raw view is off, but AdvanceChartSignalRestore called
WarriorPlotSignalLevel DIRECTLY and never checked - so every restart repainted up
to MAX_PERSISTED_ARROWS per-model opinions per member, four members per chart, on
top of the combined-vote arrows. Same shape as the vote-arrow restore bug in
322c052: a restore path that does not obey the rule its own draw path does.
Stale arrows already on the chart are purged too, since MT5 persists objects in
the profile and nothing else would ever remove them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on the live fleet at 10 bars: the restore thinned 100 of 364 and the
overlay prune 23-66 per chart, leaving 182-250 arrows over ~5000 bars. Every
layer was working; the window was simply below the ~20-bar natural spacing
between vote arrows, so it could only catch the tightest pairs.
The floor is principled, not cosmetic. A trade on this label is held for
5 + the median ZigZag leg = 18-19 bars, so any second signal inside that window
is the same trade being re-announced. 20 is the smallest defensible value and 30
is one step above it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The OOS slice is the newest history and the model never trains on it, while
online learning adapts to every bar resolving AFTER deployment. That leaves a gap
exactly at the handover, over the most regime-relevant data there is. This closes
it: select on validation, then refit on everything, which is standard practice.
Placed AFTER Net.RestoreWeights() and ResetOptimizerState() and BEFORE
PersistDeployedModel(), so it refines the weights that were actually SELECTED
rather than whatever the run happened to end on, and what it produces is what
gets written down.
THE COST IS REAL AND IS NOW STATED IN THE LOG. The deploy line promises "every
model reverts to the weights it held at the era whose combined vote scored best,
so the ensemble that trades is exactly the one that was measured". After this
pass that is no longer literally true, so the pass prints that the certified
numbers belong to the PRE-PASS weights and must be quoted that way. Set
EnableOosFinalPass=false to keep certified == traded exactly.
Guards:
* ONE-SHOT PER RUN, and the flag is set BEFORE the loop so no early return inside
it can leave the pass eligible to fire twice over bars it already trained on.
Reset at m_trainRunActive=true, because a retrain is a fresh selection and
earns a fresh pass.
* THE CONVERGED RATE, never a plateau-boosted one: m_modelEta can still carry
PLATEAU_RESTART_BOOST from an escape attempt, and this is a refinement of a
selected model, not another warm restart. g_eta is what backProp reads, so that
is what is capped and restored.
* OLDEST -> NEWEST. Series indices count backwards, so decreasing i moves forward
in time - the order the bars happened in.
* A failed feedForward is never followed by backProp; the output layer would
still hold the previous sample's activations and the update would be this bar's
label against another bar's prediction.
* m_oosFinalPassCutoff records the newest bar consumed and is deliberately NOT
cleared on a new run, so a later run can say plainly that its out-of-sample
window reaches back into bars this model has already seen.
Expect the gain to come from CURRENCY rather than finer weights: OOS precision
was measured flat from era 20 while in-sample error kept falling, so the data
this model can already see is exhausted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clutter remained after cooldown-v3 because there is a FOURTH producer of
SIG_VOTE_PREFIX arrows: CVoteArrowStore, which replays a .votearrows file and
draws into the SAME object names as the overlay. Replaying a file written before
the cooldown existed therefore resurrects exactly the arrows the prune deleted.
On a DEPLOYED chart that is the entire arrow set. The store's own header says
why: the overlay re-sweep "re-arms only when an era ends. A DEPLOYED ensemble
runs no further eras" - which is the reason this store exists at all, and also
the reason nothing would ever have removed those arrows again.
The restore now thins to the cooldown at Load(), before the progressive draw is
armed, so it is idempotent: a file already written from a cooled chart passes
through untouched, an older one is corrected once.
IT SORTS BY TIME FIRST, AND THAT IS NOT OPTIONAL. Snapshot() walks
ObjectsTotal(), so the record is in OBJECT order - its own comment says so, and
the existing MAX_KEPT trim already sorts a copy for exactly this reason. Applying
a spacing rule to an unsorted record yields negative gaps, and a negative gap is
inside any window: that is the bug that wiped 272 of 273 arrows in 2ca32e9, which
would have been reproduced here verbatim.
Insertion sort on the four parallel arrays - n is capped at VOTE_ARROWS_MAX_KEPT
(1000) and this runs once per chart per session on a path that has just done file
I/O.
Same `gap > 0` guard as the overlay prune, so a future ordering change fails
visibly by KEEPING rather than silently by deleting, and it logs what it thinned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cooldown-v2 suppressed 272 of 273 on SP500, 320 of 321 on EURUSD, 329 of 330 on
USDCAD - one surviving arrow on every chart in the fleet.
The record is OLDEST-FIRST. The prune walked it backwards, so every gap came out
NEGATIVE, and a negative gap is always <= the window: everything after the first
arrow was suppressed.
The direction was taken from the member comment on m_overlayIndex, which reads
"walking newest -> oldest" and is WRONG. The sweep DECREMENTS a SERIES index
(0 = newest) from MathMin(span, barsAvail-150) down to m_overlayStopIndex, so it
walks OLDEST -> NEWEST. The pre-existing overlay NMS at the draw site agrees -
it tests (m_overlayNmsKeptIdx - idx) and expects that to be positive for later
bars. A stale comment counts as a guess, and this one cost a build.
Comment corrected at the declaration so the next reader is not misled the same
way.
Guard added: the gap must be > 0 as well as <= the window. A non-positive gap
means the record is not in the order this loop assumes, and suppressing the whole
chart is precisely what that looks like from the outside - so it now fails
visibly by KEEPING rather than silently by deleting.
Found only because the verification was the drawn arrow count rather than an
assertion that the code was correct. Compiling clean said nothing about it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The declustering the charts needed already existed - NmsLiveAccept, per-direction
run-collapse plus cross-direction resolution plus strict alternation - and it was
already set to 10 bars. It could not be TUNED: SignalClusterWindow was a compile-
time const, so finding the right value needed a rebuild. That is the actual gap.
Now three inputs, as enum dropdowns:
Signal_CooldownScope per-direction, or a hard any-direction gate on top
Signal_CooldownBars SCB_OFF..SCB_50, default 10
Signal_CooldownMinutes SCM_OFF..SCM_1440, overrides bars when set
Minutes resolve against the CHART period and round UP, so a cooldown asked for in
wall-clock is never silently shorter than requested and survives a timeframe
change.
SCB_/SCM_ prefixes are deliberately unique. M15/M30/M60 are ALREADY members of
NF_LOOKBACK_PRESETS, and MQL5 binds a duplicated enum member to the first-declared
enum silently - the obvious names would have compiled straight into the news
filter's values.
THE ANY-DIRECTION GATE IS ADDITIVE, NOT A REPLACEMENT, and the first cut of this
had it backwards. Measured on the live log: the current rules draw 222 arrows over
4999 bars, while a BARE 10-bar cooldown permits up to 454 - because ALTERNATION is
what declutters today, not the window. Swapping the rules out would have roughly
doubled the clutter it was asked to remove. Layered, it can only ever suppress
more. Suppressed bars still advance the per-direction last-SEEN cursors, so a run
straddling the boundary does not restart as if it were fresh.
Applied at all THREE sites that must agree - live inference, OOS pass-3 scoring
and the chart renderer. Their own comments say why: an arrow set that does not
obey the same rule as the traded set shows calls the EA would never take.
Also corrects a stale comment that called this window "display only". It is not:
when it suppresses, the live path zeroes the signal outright - no arrow, no vote,
no position. Training never sees it, so these cost no retrain and are correctly
absent from the fingerprint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 5-bar conviction curve cannot answer the question it was built for. The
oracle measures ~0 at 5 bars across three charts (+0.012, -0.054, +0.064), so
PERFECT foresight earns nothing there and no rung can show payoff either. Every
reading it produced was null by construction. It was placed at 5 bars for
statistical power, before the oracle showed what that horizon is worth. Kept as
a control; the hold-horizon curve is the one to read.
Also adds MEAN DISTANCE-TO-PIVOT PER RUNG, which is the high-power form of the
same question. Payoff falls ~0.34 ATR for every bar of distance to the pivot
(fleet-pooled: d=1 +2.095, d=2 +1.743, d=3 +1.300, d=4 +0.969, d=5 +0.769,
wrong calls -0.668). So a rung that selects NEARER pivots is worth more per call
even at unchanged precision - and mean-d is a far tighter statistic than
mean-payoff, because d spans five bars where payoff spans several ATR.
That matters because it can REOPEN a lever I closed. Precision does not rise
with the rung - every 15-vs-10 comparison across six charts sits below 0.71
sigma - so the threshold looked exhausted. But precision is not the only thing a
threshold can select for. If conviction correlates with proximity to the pivot,
raising it buys payoff without buying precision.
Directional labels only: an incorrect call has no pivot and therefore no
distance, and folding those in as zero would read as "this rung picks pivots
that are imminent" when it means the opposite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ceiling on the target, and the measurement that decides where the work goes.
Same payoff arithmetic, signed by the LABEL's direction instead of the vote's,
over every directionally-labelled shared bar.
If a model that got EVERY pivot right still earns nothing over the holding
horizon then the target carries no money and no amount of model improvement
reaches any - the label, not the network, is what has to change. If the oracle
earns well the target is sound and the shortfall is the model's. Those are
completely different programmes and nothing so far distinguishes them.
It uses no forecast, so it is not a leak: it is the value of perfect foresight
OF THIS LABEL, reported as a benchmark. Nothing may trade on it.
Accumulated above the voter and direction-policy filters, like the zero-skill
book, because it is a property of the bars and their labels rather than of what
the vote did with them. A bar with no directional label offers a perfect caller
nothing to take and is skipped rather than counted as zero - the benchmark is
"every call it COULD make".
Motivated by the first skill-by-distance row, which already reframes the day:
correct calls earn +0.75 to +1.90 ATR against a spread of 0.005-0.042, and
incorrect ones cost -0.66. That puts break-even precision near 32% against a
measured 33-37% - thin, but on the right side, and utterly unlike the "no
payoff" reading the confounded 5-bar window suggested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CORRECTION to what the payoff instrument was measuring. The 5-bar horizon looked
like the powered test and it is confounded.
SwingPivotDirectionLabel returns Buy when a swing LOW lands up to
PIVOT_LABEL_TOLERANCE_BARS bars AHEAD, and says the quiet part itself: gating on
where the pivot sits relative to entry "would drop exactly the bars where the
turn has not finished coming to us", and how much adverse move remains before
the turn "is a trade-management question".
So on a CORRECT Buy call price is often still falling for d more bars. A window
shorter than d measures the APPROACH, not the leg, and its negative contribution
is expected on the calls that are RIGHT. The tight null at 5 bars
(-0.012 +/- 0.074) is therefore not evidence of no payoff. Neither horizon is
both clean and powered: 5 bars is powered and confounded, 18-19 is clean and has
an SE of 0.277.
(idx - P1) was computed in the label and thrown away. Now cached beside
m_labelResolveAge under the same validity flag, and bucketed in the era verdict.
DELIBERATELY NOT USED AS A PER-CALL HORIZON, which is the trap sitting right
next to this: d exists only on bars the label found a pivot for, so a horizon
that varied with d would hand correct and incorrect calls different windows and
bias the comparison outright. The horizon stays fixed; d only buckets.
The bucket for "the label called no pivot here" is reported by name rather than
folded in, because it is the control the others are read against. Buckets 1..N
condition on the label, so they describe the MECHANISM, not what a book earns.
Reads: rising with d means the edge is in EARLY calls and the tolerance window
is spending it - fixable by reweighting the loss, not by a new label. Flat means
that hypothesis dies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The practical question behind "can I just trade the strongest signals" is
whether payoff rises with vote magnitude. The threshold sweep already visits
every rung, so the whole curve costs four arrays and no extra pass.
Reported as the DRIFT-FREE statistic per rung - long plus short, both sign
corrected - with the two halves alongside. The halves alone invite reading a
drift-fed long side as skill, which is exactly the error the zero-skill book
caught at the certified rung: an always-long book earns MORE than the vote on
two of three charts.
Taken at the SHORT horizon, which is the one with the power. Pooled across the
three training charts the certified rung reads -0.012 +/- 0.074 ATR - a tight
null, 95% interval [-0.16, +0.13], with the long/short pattern (+0.030 against
-0.041) being the drift signature exactly. The hold horizon agrees and is 3.7x
noisier, so the answer is not a horizon artifact.
Precision is already known not to rise significantly with the rung (every
15-vs-10 comparison across six charts sits below 0.71 sigma). If payoff rises
anyway that is a surprise worth having; if it does not, the two agree and the
threshold lever is closed on both counts.
Still gates nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>