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>
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>
The by-side test is the one that separates directional skill from drift, but at
the HOLD horizon it cannot answer: payoff overlap is the horizon itself, so an
18-bar window leaves ~65 independent observations per chart and a standard error
of 0.25-0.45 ATR against an effect that would matter at 0.1.
The 5-bar window carries ~3.8x the independent observations and roughly half the
standard error. It buys that power by risking a window that ends before the
pivot has committed - which is exactly why the horizon was widened in 98f485b.
So neither horizon alone is trustworthy and both are now reported. Agreement
between them is the evidence; disagreement localises the problem to the horizon
rather than to the signal.
Measured so far, and the reason this was worth adding: the hold-horizon split
puts every chart inside one standard error - undecided, on all three - while the
zero-skill always-long book earns MORE than the vote on two of three. The raw
positive mean was drift, which is what that book was built to catch.
The drift check itself passes: base@hold / base@short lands at 3.39 and 3.41
against an expected 3.60 and 3.80, so the always-long book scales with time the
way real drift does and the payoff arithmetic is sound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ensemble beats its best single member by +2.2 to +6.8pp on all six charts -
sign-stable across six instruments, so the ensemble is doing real work rather
than diluting. How much MORE is available depends entirely on how decorrelated
the members are: the variance of an m-member average scales as (1+(m-1)r)/m, so
at r=0.8 four models are worth about 1.2 independent ones and at r=0.3 nearly 3.
Nothing measured that, so the obvious next lever - different feature subsets per
member, or a fifth architecture - could not be costed. Both force a full retrain
of 24 models, which is not a price to pay on a guess.
Measured on the SIGNED VOTE, which is what actually gets averaged: not accuracy,
not raw confidence. Two members can agree on direction almost always and still
contribute independently through magnitude.
Accumulated over every SHARED row rather than fired ones - restricting to fired
rows would measure agreement only where the members already agreed enough to
fire, which is the sample most biased toward agreement.
A member whose signed vote never varies (all abstentions, a dead tier) is
SKIPPED rather than counted as r=0, which would drag the mean toward
"decorrelated" using a member carrying no information at all.
Reported as an effective member count, which is the honest way to say what four
models are worth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
payoff-v1 reported what a call was worth and nothing to compare it against. A
positive mean R is not a finding on its own: if the instrument drifts, an
ALWAYS-LONG book earns a positive mean too, and drift is the one anomaly family
this project has found that survives cost - so the vote would be reporting the
market's own move as if it were its own.
Two comparisons, and the second is the one that decides it:
ZERO-SKILL BOOK - the same forward move accumulated with a fixed long sign over
every SHARED row, not only fired ones. Accumulated above the voter and
direction-policy filters deliberately: restricting it to bars the vote fired on
would compare the vote against a baseline the vote itself selected. Always-short
is exactly its negative, so one pass covers both.
BY SIDE - the vote's own payoff split by the direction it took, still sign
corrected, at the rung the live signal is actually trading:
both sides positive -> directional skill, it pays going either way
one positive, one negative
and roughly cancelling -> it found the drift, and the pooled mean is
saying nothing about skill
This is drift-free BY CONSTRUCTION - drift enters both sides with opposite sign
after the correction, so it cannot manufacture a two-sided positive. That is
precisely what a pooled mean cannot tell you and what no baseline subtraction
fully recovers.
The split is taken at the CHECKPOINTED rung, not this era's derived one: the
derived rung is not known until after the row loop that accumulates the split,
and the checkpointed rung is the operating point the question is actually about.
Still gates nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ensemble deploy gate certifies PRECISION against a chance rate and has
never known whether a correct call pays for its own spread. Every verdict this
project has recorded - 33% precision against a 14% chance rate, an edge that
clears its exact-binomial bar comfortably - is silent on the one question that
decides whether any of it is tradeable, and the cost boundary is exactly where
several earlier edges died with their precision already believed.
Adds a per-row payoff measurement, taken once per ROW (a chart property, not a
member one) at the same time the label is written:
* forward close move over K = round(SwingLifespanEstimate()) bars,
* the up and down extreme excursions over the same window,
each divided by the bar's own ATR. K is deliberately the label lifespan the
effective-sample-size deflation already uses, so precision and payoff describe
the same window and can be read in one sentence.
POLICY-FREE: no stop, no target, no trailing rule. It measures the SIGNAL, not
a trade-management choice layered on top - exit shaping moves payoff around
without creating any, so mixing the two would hide which was responsible.
Stored unsigned by direction; the sign comes from the vote at verdict time, and
a short's excursions SWAP rather than negate - negating them would report a
short's worst case as a negative best case.
The newest K bars of the OOS slice have no forward window and are dropped from
the tally with their own denominator, never counted as a zero move: that is the
leading-edge trap that made the lag profile's first run a false positive.
The era verdict now prints mean R, MFE and MAE at the certified rung against
the spread in the same ATR units. It GATES NOTHING - wiring a policy to an
unvalidated payoff number is how a measurement becomes a decision before anyone
has checked it.
Build tag payoff-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three charts (SP500, XAUUSD, XTIUSD) sat on the FIRST_LAYER_MIN_WIDTH floor
even after pooling took SP500 from 4.1 to 1.8 weights per independent
observation. ComputeFirstLayerWidth needs width <= ~331 to clear it; 49 columns
x 12 bars = 588.
TWO QUESTIONS, AND THE WINDOW IS NOW THE SMALLER ANSWER. The ZigZag ladder
answers "how far back is a swing worth looking" and says 12. The capacity
budget answers "how far back can this much data support" and says 6. Taking the
min stops the first writing a cheque the second cannot cover.
WHY THE LAG AXIS AND NOT THE COLUMN AXIS - the choice was between this and a
per-column mask (designed, parked on feature/column-mask):
- On the LAG axis there is a measured null. The corrected lag profile finds no
linear structure at any lag within +/-50, on all six charts, family-wise
p=1.0000, argmax scattered across different columns and lags per chart.
- On the COLUMN axis the two measures that would justify a mask - marginal MI
retention and variance share - are explicitly blind to joint and temporal
structure, and the columns they would delete include the entire price core,
which is the one place such structure would plausibly live.
Cutting where there is a measured null beats cutting where the instrument
cannot see. Corroborating: PAI/CONV/LSTM/HYBRID score within ~1pp of each
other, so the temporal machinery is not visibly earning the deeper lags.
THE CAP IS A FLEET CONSTANT, NOT A PER-CHART DERIVATION. Pool rows are keyed on
`bars x columns`, so a capacity cap computed from a chart's own observation
count would differ across the fleet by construction and hand every chart its
own layout, its own fingerprint and its own pool of one - exactly what orphaned
SP500. Set from the most starved chart; every chart shares it.
Conv survives: CONV_RECEPTIVE_FIELD_BARS is 3, so a 6-bar window still leaves 4
sliding positions. LSTM sequence length becomes 6.
RETRAIN-FORCING and POOL-INVALIDATING: width changes, so old .nnw and old
TrainPool rows are both incompatible. Wipe both - which puts the fleet back in
the cold-start condition 6c2959d was written for, and will exercise it.
Build tag -> window6-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by restarting the terminal against three charts that had just deployed -
the exact scenario d9092a2 was written for, run deliberately rather than
assumed. It failed, and the failure was mine.
SP500 swept 4999 bar(s), 4986 had a snapshot, 0 had a voter, drew 0
arrow(s). Strongest vote 0.0% against a 10.0% threshold.
The snapshot count proves d9092a2 worked: the certified edge was restored
(26.20% precision vs 13.70% chance, verified in the .stats bytes),
HasDemonstratedEdge returned true, ReconstructionWeight was non-zero and the
divisor was healthy on 4986 of 4999 bars.
But TWO readers need the member's chance rate, and I taught only one to fall
back. LiveVoteContribution still read m_eraStatChancePct DIRECTLY - era-only
state, -1 on a converged model that runs no eras - so it bailed out at
"no reference rate yet" and returned 0 for every call. The member was admitted
to the divisor and then contributed nothing to the sum: eligible, and silent.
Exactly the failure mode in feedback_rename_leaves_readers_behind, committed by
the person who wrote that note down.
Both quantities now come from one accessor each - MeasuredPrecPct() and
MeasuredChancePct() - so a third reader cannot repeat it.
ALSO: the "restored the certified edge" line was PrintVerbose. It marks a STATE
RESTORE, which by this codebase's own rule never sits behind the verbose gate,
and its absence from the log was briefly read as evidence the restore had not
happened. Promoted to Print.
Build tag -> voterestore-v1. Not a layout change: no retrain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resurrecting the diagnostic in 4113afd immediately showed why it needed more
than resurrecting. Its first output, on every chart:
strongest of 49 columns x 7999 lags is column 4 at lag +3900, |r| 0.5120
(32.4 SE of the 0.0158 no-information band) -> SURVIVES
XTIUSD and SP500 named the SAME column at the SAME lag with |r| within 0.01 of
each other. Two independent instruments cannot agree to that precision at a
3900-bar lag; that is what identified it as an artifact rather than a finding.
TWO DEFECTS, both of which manufacture significance at long lags.
1. THE NO-INFORMATION BAND WAS GLOBAL, THE CORRELATION IS NOT.
CorrR1D is non-circular: lag k is computed from (n - |k|) overlapping terms
while the normaliser uses all n. Scoring every lag against one 1/sqrt(n)
band understates it by sqrt(n/(n-|k|)) - a factor of 6 at the edge. Lag
+3900 of 3999 rests on ~100 overlapping terms and was being judged as if it
rested on 4000. Each lag now gets its own band, deflated for label
persistence, and the maximum is ranked on z rather than |r| - two equal
correlations are not equally surprising on different sample sizes.
2. THE LAG RANGE ANSWERED NO QUESTION.
The profile exists to say how deep a lookback carries linear structure, and
the input window is HistoryBars. Lags two orders of magnitude past it are
both meaningless and where this estimator is worst. Capped at 4x the window.
The Sidak family is now the lags actually searched, not 2n-1.
Left deliberately: this remains a LINEAR, marginal measure. It can bound the
useful lookback; it cannot prove a shorter window loses nothing.
Build tag -> lagprofile-v2. Not a layout change: no retrain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every chart, every era, on every run in the logs: "linear lag profile skipped -
only 0 contiguous OOS bars." That reads as "not enough data". It was not. The
walk never started.
ReportLinearLagProfile walks newest-first from r=0 and breaks on the first row
without a label, to avoid splicing across a hole. But r=0 IS the newest bar,
and a forward-looking swing-pivot label cannot be resolved there by
construction - the opposite pivot has not committed yet. So HasLabel(0) is
false, the loop breaks on its first iteration, and n=0. Permanently.
The leading gap is SYSTEMATIC (always about the label resolution), not a hole
in the middle of the series, so stepping over it splices nothing. Contiguity is
still enforced from the first labelled row onward.
WHY THIS MATTERS BEYOND THE DIAGNOSTIC: the input window is 12 bars, and the
capacity budget divides by width = columns x bars. Cutting the window is the
largest lever left for the three charts still pinned to the 16-unit first-layer
floor, and there has been no measurement of whether the deeper lags carry
anything - because the one diagnostic that would answer it has never produced a
number. The old lag verdict in memory predates the pivot-event label.
The skip message now reports where the walk ran out, so "0 from r=0" (never
started) is distinguishable from "0 from r=37" (genuinely short window).
Build tag -> lagprofile-v1. NOT a feature-layout change: no retrain, models
resume from their weights and the training pool stays valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator report: arrows labelled as restored from a previous session on a
fleet training from era 0. Confirmed - all six charts restored 115-431
combined-vote arrows drawn by models that no longer exist.
TWO INDEPENDENT DEFECTS, either of which alone causes it.
1. CVoteArrowStore::Discard() HAD NO CALLER.
The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a
fresh topology. The CHART-scoped .votearrows store has an equivalent
Discard(), written for exactly this, and nothing ever called it. The store
is keyed on the DB config fingerprint, which does not move when a model is
wiped, so it reloaded across any reset - fresh topology, panel weight reset,
or a model-file wipe.
A vote is a claim made by a specific set of members. If any member rebuilt
from scratch this run, the whole stored history is void, so
g_warriorFreshTopologyThisRun is now raised wherever a member discards
weights or builds a fresh topology, and the store Discards instead of Loads.
2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\.
.sigvis and .votearrows were written to the ROOT of Common\Files, outside
the one directory that "wipe the Warrior EA files" has always meant. Two
consecutive wipes this session left them standing untouched, and neither
wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\.
A wipe that does not remove all of a program's state is not a wipe, and
nothing in the log told the operator which files were missed.
NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and
Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and
should be deleted once.
Build tag -> fleet-pool-v3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. THE POOL FIX WAS LANDING ON A TOPOLOGY THAT COULD NOT SEE IT.
ComputeFirstLayerWidth budgets against EstimatedInSampleBars, which counts
this chart's own bars PLUS the training pool. On a COLD fleet start every
chart derives and pins its topology BEFORE any chart has published a pool
file - measured on the 18:13 start, model creation at 18:13:21 against a
first publish at 18:13:48. All six sized as if training alone, wrote that
into .cfg, and adopted it back on every later start even with the pool full.
SP500 ran a first layer floored to 16 while adopting 30229 peer rows.
Adopt-don't-compare exists to protect weights shaped by those sizes. It was
also running for a model with NO .nnw, where there is nothing to protect and
the .cfg is just a record of one unlucky moment. The four derived sizes are
now re-measured when no weights exist.
Safe on all three counts that matter: free (nothing to discard), cannot loop
(once weights exist the .cfg is authoritative again), and cannot fragment the
pool - the derived width is NOT in BuildModelFingerprint, which keys only on
the FEATURE layout. Verified: field 2 of the fingerprint is
LEGACY_HISTORY_BARS_SLOT, not the first-layer width.
TO TAKE EFFECT the weights must be wiped while the TrainPool is KEPT - the
census has to be non-empty at derivation time. A full wipe empties the pool
and reproduces the original condition exactly.
2. THE KEEP-SCREEN LATCHED ON AN UNDERPOWERED SAMPLE.
MI_MIN_SAMPLES is a floor for "can this be computed", and it was being used
as the bar for "is this answer final". The screen fired on the first era
clearing 200 rows and latched, measuring at 202-773 samples where a warm
chart gives ~2065. Columns kept then tracked SAMPLE SIZE rather than
information - EURUSD kept 0 of 49 at n=202, SP500 kept 15 at n=773, and the
ordering across all six charts was very nearly monotone in n.
A thin sample is still measured and printed, but it no longer closes the
question: below MI_GOOD_SAMPLE_FRACTION of the target the result is labelled
underpowered and a later era supersedes it, bounded by the same attempt
budget. An underpowered screen that latches is worse than one that waits,
because it looks like a result.
Build tag -> fleet-pool-v2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.
1. SP500 was training alone, and one alt-data column was the reason.
The alt block's width joins the model fingerprint, and the pool reader only
adopts peer rows whose fingerprint and width match. The exporter gives each
instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
13 - so the fleet ran as three incompatible pools:
EURUSD/USDJPY/USDCAD adopt ~57-60k peer rows each
XAUUSD/XTIUSD adopt 6.4k / 20.3k
SP500 "EVERY peer file was REJECTED, so this chart is
training alone" - 0 rows
SP500 therefore trained on 2279 independent observations against a 600-wide
input with its first layer floored at 16, printing its own "expect
overfitting" warning. It is the one chart with no pool and the worst
capacity ratio in the fleet by a factor of three.
Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
instead of their own file header. An existing model still adopts its .cfg
pin, so this re-keys nothing that is already trained.
Intersection rather than union: filling an absent series with its median
makes that column constant per instrument, which lets a pooled model
identify the source instrument and stop learning the shared mechanism. It
is also 6 columns narrower. Cost is six columns whose retained information
is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
to names.
2. The MI keep-screen disabled itself for the whole run on any cold start.
ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
the label cache is allocated before it is filled, so BuildMiSample finds no
row carrying a resolved label and returns 0 - a sixth exit, and the only
one the 8c1266d instrumentation did not cover, which is why it printed
nothing. observed then stayed -1, the permutation loop never iterated, and
the report emitted "-1.00000 nats over 0 permutations" beside a plausible
"strongest single feature 0.05979" that was a STALE m_miBestColumn from an
earlier scoring call. The first ensemble member propagated the latch to
g_ensembleChartMiReportDone and silenced every member on the chart.
The flag now latches only once a measurement exists. A short sample is
reported as a deferral naming the two numbers that identify it (cached bars
vs bars carrying a resolved label) and retried, up to
MI_REPORT_MAX_ATTEMPTS.
Build tag -> fleet-pool-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
REPLACES the in-line retry from ad4ae58, which was the wrong shape and did not
work. Measured after deploying it:
atomic rename ... failed (error 5004) after 4 attempts
MQL5 exposes no FILE_SHARE_DELETE, so a rename CANNOT succeed while any reader
holds the destination open - it is not a lock that waiting longer wins. The
retry assumed a peer holds a pool file for "tens of ms"; USDCAD_16388.bin is
134 MB and a peer reading it holds the handle for SECONDS. The loop lost every
time and bought nothing but 75ms of tick latency on the failure path.
The content is already written and correct - only the SWAP is blocked. So try the
rename once, and on failure remember the temp and promote it from OnTimer, where
I/O belongs. Once the reader closes, a single FileMove lands it. That beats the
old fallback of waiting for the next full publish, which rewrites all 134 MB and
may be an era away.
* pending list is bounded (8) and deduplicated - AtomicWriteBegin reuses one
temp name per file, so a second failure for the same file must not take a
second slot. A full list falls back to the previous next-publish behaviour.
* a successful write FORGETS any queued promotion for that name, so a stale
temp can never overwrite fresher content.
* a vanished temp (a later publish succeeded outright) is dropped, not retried.
* a landed promotion is LOGGED. Silence is what made me misread the last
attempt as working when there had simply been no contention in the window.
Compiled clean; NOT yet run - and note that verification needs a collision to
occur, which happened ~27 times across a whole day. Absence of the message in any
one window is not evidence either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second instance of the same regression b1c3a89 fixed in the .stats loader, found
by reading the log after it: the arrow store was still configured with the
Signal_ThresholdOpen SEED while the chart traded a derived, pinned rung.
SP500: queued 366 combined-vote arrows for progressive restore
(threshold to open 25%) <- chart was trading 15%
CVoteArrowStore carries the identical compare-and-discard guard - "an arrow is a
claim about a threshold, so a changed threshold makes every stored arrow a claim
about a strategy that is no longer configured". Handing it the seed therefore did
both harms at once: discarded restorable arrows whose stored threshold was the
real one, and labelled whatever survived with a threshold the EA does not use.
Root cause is the same in both places: c6eb908 changed the threshold from an
input into a derived value, and two separate consumers still assumed the input.
Worth remembering as the shape of this bug rather than the instances - anything
that PERSISTED the old threshold had to be re-checked, not just anything that
read it.
Reads g_ensDerivedThreshold, which LoadModelStats() has already restored by this
point in init (its "restored the ensemble record" line prints before the arrow
queue line, which is what makes reading it here safe), and falls back to the seed
before the first era has ever been scored. Display-only: the trade path already
had the right number from PublishVoteThreshold().
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.
Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.
Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.
Fallbacks, both of which stay stable without a file:
* tester/optimizer/forward use a magic derived from chart identity, so two
identical passes cannot differ.
* an unwritable file falls back to that same derived value, and says so.
Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.
WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.
Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Added bulk read/write methods for feature caches in IFeaturesView and its implementations to optimize performance.
- Introduced LabelCacheInvalidateAll method to manage label cache invalidation alongside feature cache.
- Implemented PooledIndependentBars method in topology interfaces to account for additional independent observations.
- Enhanced risk budget management with throttling for peak-equity updates to reduce unnecessary file operations.
- Improved error handling and logging for ATR trailing stops to ensure better visibility of issues.
- Updated alt-data handling to prevent unnecessary operations during testing and optimization phases.
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TrainingTarget defaults to TARGET_SWING.
- LogitAdjustTau input, preset enum and all plumbing deleted: tau is fixed
at 1.0 (the full log-prior, Menon et al.'s consistent value); the
delivered strength is capped to the head's usable logit range from the
priors the prebuild measures. The CAPPED journal line is the step-1
measurement. |LA💯BS becomes a frozen legacy fingerprint slot, so no
existing model re-keys.
- The swing label measures its own resolution lag (idx - P2, the earliest
bar P1 can be final on) into the overlap/SE machinery, capped at
SWING_SCAN_CAP_BARS instead of a barrier horizon it does not have.
- The prebuild line is target-aware: both-won, timeout and horizon-lifespan
fragments are barrier-walk facts and no longer decorate swing counts.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TARGET_SWING: the direction models learn which way the next CONFIRMED SWING
PIVOT lies from the current close. Geometry-free - the label owes nothing to a
stop, target or horizon - which is what lets trade management be tuned
separately instead of being baked into what the net learns.
SwingPivotDirectionLabel reuses the ZigZag pivot the horizon and leg-size
measurement already walk, so there is ONE notion of "pivot" in the codebase. It
walks forward in time and stops at m_swingConfirmationBars: a pivot nearer than
that is still repainting, so its label is not knowable yet and the bar stays
Neutral. That boundary is the whole lookahead control for this target.
TrainingTarget input is back (TARGET_BARRIER default, unchanged behaviour) with
TARGET_FRACTAL and TARGET_SWING beside it; |TGT:SWG1 joins the fingerprint so
switching trains a separate model rather than relabelling an existing one.
ADZigZag was renamed to ZigZag throughout (30 identifiers). It has loaded
MetaTrader's stock Examples\ZigZag at its stock defaults for some time - the
migration was done, only the name was left behind, and a name that says "AD"
about a stock indicator is exactly the legacy pointer this codebase should not
carry. No behaviour change: same #resource, same params.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta,
ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure,
WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a
closed verdict: the three oscillators are the same patterns that measured at chance
as entries, and the Wyckoff family returned zero out-of-sample on five independent
instruments - which is what closed the context score.
RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks
larger than it is. Every removed group contributed `flag ? N : 0` to the input
width, and every flag was false, so the width was ALREADY zero for all eight: no
.nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were
hashed unconditionally and become literal 0 legacy slots (the convention the
m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only
when enabled, so their segments simply never appear - byte-identical to every
fingerprint ever produced, since neither ever shipped on.
CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted
inside every .nnw, and Unflatten() rejects a size mismatch by falling back to
constructor defaults - so dropping the dead fields would silently revert the tuned
MA period of every model on disk while keeping its trained weights. That is the
feature/weight mismatch this project has already paid for twice, and it is not
worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and
read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing
searches them. The class comment says all of this at the declaration.
Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one
indicator now, and a name saying "AD" for the MA handle is the kind of stale label
that gets believed later. Its release-AFTER-recreate ordering is untouched - that
is a documented fix, not bookkeeping.
Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0
baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both selected on an enum via nested if/else-if chains 2-3 levels deep,
inconsistent with the switch-dispatch style the rest of the file uses
(HandleControlPanelAction, 2f1951e). InitializeTrailing's ATR
x1/x2/x3 multiplier also collapsed from 3 sequential equality checks
into its own small switch. Pure control-flow reshape: same branches,
same bodies, same fallthrough-to-true default - no behavioral change.
All three MM_STRATEGY branches repeated new+null-check+Expert.InitMoney()+
null-check verbatim. Added CreateAndInitMoney<TMoney>(functionName), the
same template-helper shape as the existing CreateSignalWithRetry<TSignal>
a few hundred lines up. Pure relocation - same error text, same control
flow, only the branch-specific setter calls (Percent/Lots/UseAIConfidence...)
stay inline.
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
The ~185-line switch mixed chart-UI toggling, AI training-lifecycle
dispatch, weight save/load/reset and DB/report admin in one function
with subtly different guard conditions per branch. Each CP_ACTION_*
case is now its own HandleCp*() free function (matching this file's
existing procedural style - ConfirmDestructiveAction, RefreshControlPanelLabels,
etc. are already standalone functions over the same globals); the
switch is now a one-line-per-case dispatch table. Every guard/confirm/
Alert/Print sequence is preserved verbatim (break -> return only
change; verified via quoted-string-set diff = empty and if/Alert/
Print/DispatchSignalCommand counts identical against the original).
Compiled 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert.Init() and the main signal's `new CExpertSignalCustom` each had their own
5-retry loop, hand-rolled, while CreateSignalWithRetry<T>() and RetryInitStep()
already exist and are used for every OTHER signal/init step in this same function.
Expert.Init() -> a StepExpertInit() shim through RetryInitStep (same pattern as
StepInitTrailing/StepInitIndicators/etc. - and picks up RetryInitStep's fatal-reason
fast-fail, which the hand-rolled loop didn't have: a permanent AcquireConfigLock
refusal now fails immediately instead of blindly retrying 5 times).
`new CExpertSignalCustom` -> CreateSignalWithRetry<CExpertSignalCustom>(maxRetryOnError,
true), the exact template already used for PAI/CONV/LSTM/HYBRID/META/MA/RSI/MACD/
Ichimoku/NewsFilter/SessionFilter/RiskGuard. The dbm.OpenDatabase/BeginTransaction/
CommitTransaction/CloseDatabase loop stays hand-rolled - it's a multi-step
transactional retry with different per-step cleanup, not a single-op retry, so it
does not fit either existing helper's shape. Compiled clean (0 errors, 0 warnings).